Agent Wranglers

Required

Accept push endpoints only from known push services

Before you store a web push subscription, check that its endpoint is HTTPS on a push service host you allow. Refuse every other address.

Risk if broken

An account or system taken over

How to do it

The browser sends the endpoint, so an attacker can send any address and make your server POST to it later. Keep an allow list of the push services your supported browsers use, such as fcm.googleapis.com, updates.push.services.mozilla.com and *.push.apple.com. Apple names *.push.apple.com as the hosts a sending server must reach. Refuse any other host, any port, and any http: address, and do not follow redirects when you send. This is the push form of Fetch only allowed addresses.

Examples

Using these examples: they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

JavaScript example

const PUSH_HOSTS = ['fcm.googleapis.com', 'updates.push.services.mozilla.com'];
const PUSH_HOST_SUFFIXES = ['.push.apple.com', '.notify.windows.com'];

export function isPushEndpoint(endpoint) {
  let url;
  try { url = new URL(endpoint); } catch { return false; }
  if (url.protocol !== 'https:' || url.port !== '') return false;
  return PUSH_HOSTS.includes(url.hostname) || PUSH_HOST_SUFFIXES.some(suffix => url.hostname.endsWith(suffix));
}

Python example

from urllib.parse import urlsplit

PUSH_HOSTS = {"fcm.googleapis.com", "updates.push.services.mozilla.com"}
PUSH_HOST_SUFFIXES = (".push.apple.com", ".notify.windows.com")

def is_push_endpoint(endpoint: str) -> bool:
    url = urlsplit(endpoint)
    if url.scheme != "https" or url.port is not None or not url.hostname:
        return False
    return url.hostname in PUSH_HOSTS or url.hostname.endswith(PUSH_HOST_SUFFIXES)

How to check

An API test posts subscriptions with the endpoints https://169.254.169.254/latest, http://fcm.googleapis.com/fcm/send/x and https://push.example.com/x. Each one gets 422, and no row is stored.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/notifications/push-notifications/push-endpoint-allow-list">Agent Wranglers notifications playbook</a>. In code you write, add a comment next to it with the same link.