You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Establishing this required a write, so I made one. There is now a junk record in the production SUBSCRIPTIONS namespace pointing at https://example.invalid/x. Its key is sub: + SHA-256 of that endpoint. It has no TTL and nothing prunes it, which is part of the finding. Please drain it.
No session, no origin check, and no proof that the endpoint is a push service.
Why this is #13's invariant, in a path #13 did not survey
#13 concludes, for the feed read: "No client input reaches the outbound fetch. The URL comes from env.FEED_URL / env.PRS_FEED_URL — configuration, never the request." That is correct. I confirmed it in src/worker.js.
It is not true of the push fan-out. src/push.js builds its request from the stored endpoint:
So an anonymous POST to /subscribe chooses a host the Worker will later fetch, and audienceFor mints a VAPID JWT whose aud is the caller's own origin, signed with VAPID_PRIVATE_KEY and then delivered to them. #13 called the steerable-fetch case "SSRF, not just amplification", then established that the property held three times over — for the path it looked at. There are two outbound paths and it surveyed one.
The check that is claimed vs the check that runs
src/subscriptions.js is explicit about the property it is defending:
// https only. A push service is always https, and accepting anything else// would let a subscribe call point this Worker's sender at an arbitrary host.if(url.protocol!=="https:")return{ok: false,error: "endpoint must be https"};
The comment names the right risk. The check stops one step short of it: it constrains the scheme, not the host. It excludes http://attacker and admits https://attacker. This is the shape docs/agentic-code-hygiene.md rule 3 is about — a gate's own claim about itself is not evidence. The rationale is sound; the code does not implement it.
Second-order: the store only grows
putSubscription writes with no expirationTtl, and per src/push.js only a 404 or 410 from the push service is allowed to delete. A hostile or merely inert endpoint returns neither, so a record written this way is permanent. Distinct endpoints hash to distinct keys, so the dedupe-by-hash design — correct for real devices — is exactly what makes anonymous writes unbounded. Every subsequent fan-out pays for every junk record.
Cold read of the live site, 2026-09-03.
Disclosure first
Establishing this required a write, so I made one. There is now a junk record in the production
SUBSCRIPTIONSnamespace pointing athttps://example.invalid/x. Its key issub:+ SHA-256 of that endpoint. It has no TTL and nothing prunes it, which is part of the finding. Please drain it.What happens
No session, no origin check, and no proof that the endpoint is a push service.
Why this is #13's invariant, in a path #13 did not survey
#13 concludes, for the feed read: "No client input reaches the outbound fetch. The URL comes from
env.FEED_URL/env.PRS_FEED_URL— configuration, never the request." That is correct. I confirmed it insrc/worker.js.It is not true of the push fan-out.
src/push.jsbuilds its request from the stored endpoint:So an anonymous POST to
/subscribechooses a host the Worker will later fetch, andaudienceFormints a VAPID JWT whoseaudis the caller's own origin, signed withVAPID_PRIVATE_KEYand then delivered to them. #13 called the steerable-fetch case "SSRF, not just amplification", then established that the property held three times over — for the path it looked at. There are two outbound paths and it surveyed one.The check that is claimed vs the check that runs
src/subscriptions.jsis explicit about the property it is defending:The comment names the right risk. The check stops one step short of it: it constrains the scheme, not the host. It excludes
http://attackerand admitshttps://attacker. This is the shapedocs/agentic-code-hygiene.mdrule 3 is about — a gate's own claim about itself is not evidence. The rationale is sound; the code does not implement it.Second-order: the store only grows
putSubscriptionwrites with noexpirationTtl, and persrc/push.jsonly a 404 or 410 from the push service is allowed to delete. A hostile or merely inert endpoint returns neither, so a record written this way is permanent. Distinct endpoints hash to distinct keys, so the dedupe-by-hash design — correct for real devices — is exactly what makes anonymous writes unbounded. Every subsequent fan-out pays for every junk record.Proposed
*.push.services.mozilla.com,*.notify.windows.com,fcm.googleapis.com,*.push.apple.com). This is the check the existing comment already describes, and it restores Pin the invariant that a client cannot cache-bust to origin, or steer the Worker's outbound fetch #13's invariant across both outbound paths./subscribewith adversarial endpoints and assert that nothing outside the allowlist is ever stored or fetched. Pin the invariant that a client cannot cache-bust to origin, or steer the Worker's outbound fetch #13 asks for that pin on the feed path; this is the same pin on the push path.Not measured
Whether Cloudflare rate-limits this route in front of the Worker. That would bound the write rate but not the property.