Skip to content

fix: validate subscription endpoint before persisting it - #89

Open
palmoni5 wants to merge 1 commit into
NodeBB:mainfrom
palmoni5:fix/validate-subscription-endpoint
Open

fix: validate subscription endpoint before persisting it#89
palmoni5 wants to merge 1 commit into
NodeBB:mainfrom
palmoni5:fix/validate-subscription-endpoint

Conversation

@palmoni5

Copy link
Copy Markdown

Subscriptions.add writes subscription.endpoint to the database exactly as it arrives from POST /api/web-push/subscription:

Subscriptions.add = async (uid, subscription, device) => {
	const { endpoint } = subscription;
	const entry = { ...subscription, ...(device || {}) };
	await Promise.all([
		db.sortedSetAdd(`uid:${uid}:web-push:subscriptions`, Date.now(), endpoint),
		db.setObject(`web-push:subscriptions:${endpoint}`, entry),
		...

Nothing validates it. plugin.onNotificationPush then hands the stored subscription straight to webPush.sendNotification for every notification the user receives, so an authenticated user can register an arbitrary url and get the server to POST to it repeatedly, at whatever rate they can generate notifications for themselves.

POST /api/web-push/test already runs request.check() before sending, but that only covers the explicit test button — the write path and the regular push path had no check at all, which is the inconsistency this fixes.

web-push requiring https and validating certificates does limit what an attacker gets out of it in practice; plain-HTTP internal targets aren't reachable this way. It's still a stored, user-controlled request target that the server hits on a schedule the attacker influences.

The fix validates at the single point where endpoints enter storage, so the push path needs no per-notification check:

  • must parse as a url
  • must be https:
  • must pass request.check(), core's reserved-IP-range / DNS-rebinding guard
  • otherwise add() throws [[error:invalid-url]] and nothing is written

It also trims the endpoint on both add and remove, so the sorted set member and the web-push:subscriptions:<endpoint> key stay in sync with what the test route (which already trims) looks up.

This only guards new writes — endpoints stored before this change are unaffected, and the request.check() in the test route stays in place for them.

`Subscriptions.add` stored `subscription.endpoint` exactly as received.
The endpoint is caller-supplied, and `plugin.onNotificationPush` later
POSTs to it for every notification the user receives, so an authenticated
user could register an arbitrary url and have the server request it
repeatedly on their behalf.

`POST /api/web-push/test` already runs `request.check()` before sending,
but nothing guarded the write path, so the regular push path had no check
at all.

Reject the endpoint at `add()` unless it parses as a url, uses https, and
passes `request.check()` (core's reserved-IP-range/DNS-rebinding guard).
Also trim the endpoint on both add and remove so the sorted set member
and the object key stay in sync with what the test route looks up.
@barisusakli
barisusakli requested a review from julianlam August 27, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant