fix(push): stop web push from posting to arbitrary endpoints
This commit is contained in:
parent
1afaea7214
commit
043ef579a8
7 changed files with 418 additions and 34 deletions
|
|
@ -5,6 +5,7 @@
|
|||
// VAPID and encrypt per RFC 8291. No Google account or Firebase project needed.
|
||||
|
||||
import { PushContractError, type PushNotification } from './push-contract.ts'
|
||||
import { evaluateWebPushEndpoint } from './webpush-endpoint-policy.ts'
|
||||
|
||||
export interface WebPushConfig {
|
||||
/** base64url encoded uncompressed P-256 public key (65 bytes, 0x04 prefix). */
|
||||
|
|
@ -117,6 +118,17 @@ export function readWebPushConfig(
|
|||
}
|
||||
}
|
||||
|
||||
function readSubscriptionKey(keys: unknown, name: 'p256dh' | 'auth'): string {
|
||||
if (!keys || typeof keys !== 'object') return ''
|
||||
const value = (keys as Record<string, unknown>)[name]
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a stored Web Push subscription. The endpoint must pass the push
|
||||
* service allowlist (see webpush-endpoint-policy.ts); anything else is
|
||||
* treated as a stale registration so it is purged without any network IO.
|
||||
*/
|
||||
export function parseWebPushSubscription(registrationId: string): WebPushSubscription {
|
||||
if (typeof registrationId !== 'string' || registrationId.length < 20 || registrationId.length > 8192) {
|
||||
throw new PushContractError('webpush_registration_invalid', 400, true)
|
||||
|
|
@ -124,18 +136,13 @@ export function parseWebPushSubscription(registrationId: string): WebPushSubscri
|
|||
try {
|
||||
const parsed = JSON.parse(registrationId) as Record<string, unknown>
|
||||
const endpoint = typeof parsed.endpoint === 'string' ? parsed.endpoint : ''
|
||||
const keys = parsed.keys
|
||||
const p256dh = keys && typeof keys === 'object' && typeof (keys as Record<string, unknown>).p256dh === 'string'
|
||||
? (keys as Record<string, string>).p256dh
|
||||
: ''
|
||||
const auth = keys && typeof keys === 'object' && typeof (keys as Record<string, unknown>).auth === 'string'
|
||||
? (keys as Record<string, string>).auth
|
||||
: ''
|
||||
const endpointUrl = new URL(endpoint)
|
||||
const p256dh = readSubscriptionKey(parsed.keys, 'p256dh')
|
||||
const auth = readSubscriptionKey(parsed.keys, 'auth')
|
||||
const verdict = evaluateWebPushEndpoint(endpoint)
|
||||
const p256dhBytes = decodeBase64Url(p256dh)
|
||||
const authBytes = decodeBase64Url(auth)
|
||||
if (
|
||||
endpointUrl.protocol !== 'https:'
|
||||
!verdict.allowed
|
||||
|| p256dhBytes.byteLength !== 65
|
||||
|| p256dhBytes[0] !== 0x04
|
||||
|| authBytes.byteLength < 16
|
||||
|
|
@ -143,7 +150,7 @@ export function parseWebPushSubscription(registrationId: string): WebPushSubscri
|
|||
) {
|
||||
throw new Error('invalid subscription')
|
||||
}
|
||||
return { endpoint: endpointUrl.toString(), p256dh, auth }
|
||||
return { endpoint: verdict.url.toString(), p256dh, auth }
|
||||
} catch (error) {
|
||||
if (error instanceof PushContractError) throw error
|
||||
throw new PushContractError('webpush_registration_invalid', 400, true)
|
||||
|
|
@ -289,6 +296,8 @@ export async function sendWebPushMessage(
|
|||
TTL: '86400',
|
||||
},
|
||||
body,
|
||||
// Never follow a redirect off the allowlisted push service host.
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
})
|
||||
} catch {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue