// server/supabase/functions/_shared/webpush-redteam-r1-19.test.ts // Regression: a user-supplied Web Push endpoint must never make the edge // function POST to an arbitrary host (SSRF with a status oracle). import { parseWebPushSubscription, sendWebPushMessage, type WebPushConfig } from './webpush.ts' import { evaluateWebPushEndpoint, isAllowedWebPushHost, type WebPushEndpointRejection, } from './webpush-endpoint-policy.ts' import { PushContractError, type PushNotification } from './push-contract.ts' function assert(condition: boolean, message: string): asserts condition { if (!condition) throw new Error(message) } function b64url(bytes: Uint8Array): string { let binary = '' for (const byte of bytes) binary += String.fromCharCode(byte) return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '') } async function makeKeys(): Promise<{ p256dh: string; auth: string }> { const pair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']) const raw = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey)) return { p256dh: b64url(raw), auth: b64url(crypto.getRandomValues(new Uint8Array(16))) } } async function makeVapidConfig(): Promise { const pair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']) const publicRaw = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey)) const jwk = await crypto.subtle.exportKey('jwk', pair.privateKey) assert(typeof jwk.d === 'string', 'private scalar must be exportable') return { publicKey: b64url(publicRaw), privateKey: jwk.d, subject: 'mailto:push@d3ro.test' } } const notification: PushNotification = { title: 'Transcription complete', body: 'Open D3RO Voice', data: { schema_version: '1', event_type: 'transcription.completed', resource_id: '11111111-2222-4333-8444-555555555555', route: 'HistoryDetail', history_id: '11111111-2222-4333-8444-555555555555', }, } const HOSTILE_ENDPOINTS: ReadonlyArray<[string, WebPushEndpointRejection]> = [ ['https://internal-host:8443/x', 'endpoint_non_default_port'], ['https://internal-host/x', 'endpoint_host_not_allowed'], ['https://127.0.0.1/x', 'endpoint_ip_literal'], ['https://0x7f.1/x', 'endpoint_ip_literal'], ['https://2130706433/x', 'endpoint_ip_literal'], ['https://[::1]/x', 'endpoint_ip_literal'], ['https://169.254.169.254/latest/meta-data', 'endpoint_ip_literal'], ['https://fcm.googleapis.com:8443/fcm/send/abc', 'endpoint_non_default_port'], ['https://user:pw@fcm.googleapis.com/fcm/send/abc', 'endpoint_has_credentials'], ['https://fcm.googleapis.com.attacker.example/fcm/send/abc', 'endpoint_host_not_allowed'], ['https://evilfcm.googleapis.com/fcm/send/abc', 'endpoint_host_not_allowed'], ['https://attacker.example/notify.windows.com', 'endpoint_host_not_allowed'], ['https://notify.windows.com/x', 'endpoint_host_not_allowed'], ['https://push.example.com/subscriptions/abc123', 'endpoint_host_not_allowed'], ['http://fcm.googleapis.com/fcm/send/abc', 'endpoint_not_https'], ['not a url', 'endpoint_unparseable'], ] const LEGITIMATE_ENDPOINTS = [ 'https://fcm.googleapis.com/fcm/send/abc123', 'https://FCM.googleapis.com:443/fcm/send/abc123', 'https://updates.push.services.mozilla.com/wpush/v2/gAAAA', 'https://updates-autopush.push.services.mozilla.com/wpush/v2/gAAAA', 'https://wns2-by3p.notify.windows.com/w/?token=BQYAAA', 'https://web.push.apple.com/QGuQyavXutnMH', ] Deno.test('endpoint policy rejects hosts outside the push service allowlist', () => { for (const [endpoint, reason] of HOSTILE_ENDPOINTS) { const verdict = evaluateWebPushEndpoint(endpoint) assert(!verdict.allowed, `${endpoint} must be rejected`) assert(verdict.reason === reason, `${endpoint}: expected ${reason}, got ${verdict.reason}`) } }) Deno.test('endpoint policy accepts the browser vendor push services', () => { for (const endpoint of LEGITIMATE_ENDPOINTS) { const verdict = evaluateWebPushEndpoint(endpoint) assert(verdict.allowed, `${endpoint} must be allowed`) } assert(isAllowedWebPushHost('WEB.PUSH.APPLE.COM'), 'host match is case-insensitive') assert(!isAllowedWebPushHost('10.0.0.1'), 'IP literal is never an allowed host') }) Deno.test('subscription parser refuses a hostile endpoint as a stale registration', async () => { const keys = await makeKeys() for (const [endpoint] of HOSTILE_ENDPOINTS) { let caught: unknown try { parseWebPushSubscription(JSON.stringify({ endpoint, keys })) } catch (error) { caught = error } assert(caught instanceof PushContractError, `${endpoint} must throw`) assert(caught.code === 'webpush_registration_invalid', `${endpoint}: code ${caught.code}`) assert(caught.staleRegistration === true, `${endpoint}: must be purged as stale`) } const parsed = parseWebPushSubscription( JSON.stringify({ endpoint: 'https://fcm.googleapis.com:443/fcm/send/abc123', keys }), ) assert(parsed.endpoint === 'https://fcm.googleapis.com/fcm/send/abc123', 'endpoint normalized') }) Deno.test('send never reaches the network for a hostile endpoint', async () => { const keys = await makeKeys() const config = await makeVapidConfig() let fetchCalls = 0 const spyFetch = (() => { fetchCalls += 1 return Promise.resolve(new Response(null, { status: 201 })) }) as unknown as typeof fetch let caught: unknown try { await sendWebPushMessage( JSON.stringify({ endpoint: 'https://internal-host:8443/x', keys }), notification, { config, fetchImpl: spyFetch }, ) } catch (error) { caught = error } assert(caught instanceof PushContractError, 'hostile endpoint throws') assert(caught.code === 'webpush_registration_invalid', `unexpected code ${caught.code}`) assert(caught.staleRegistration === true, 'hostile registration is purged, not retried') assert(fetchCalls === 0, 'no request may be sent to a hostile endpoint') }) Deno.test('send refuses to follow redirects off the push service', async () => { const keys = await makeKeys() const config = await makeVapidConfig() let redirectMode: RequestRedirect | undefined const spyFetch = ((_input: string, init?: RequestInit) => { redirectMode = init?.redirect return Promise.resolve(new Response(null, { status: 302, headers: { Location: 'https://internal-host/' } })) }) as unknown as typeof fetch let caught: unknown try { await sendWebPushMessage( JSON.stringify({ endpoint: 'https://fcm.googleapis.com/fcm/send/abc123', keys }), notification, { config, fetchImpl: spyFetch }, ) } catch (error) { caught = error } assert(redirectMode === 'manual', 'redirects must not be followed automatically') assert(caught instanceof PushContractError && caught.code === 'webpush_send_failed', 'redirect is a send failure') })