From d6acc1b1eb62eba5c34170b5b7c208aa5b85a054 Mon Sep 17 00:00:00 2001 From: atul-upadhyay-7 Date: Sun, 21 Jun 2026 15:57:43 +0530 Subject: [PATCH 1/2] fix: remove notification management token from URL query parameters Removes the query parameter fallback (managementToken, token) from getNotificationManagementToken(). Tokens are now only accepted via: - x-notification-token HTTP header - managementToken field in the POST request body URL query parameters are logged by servers, CDNs, and proxies, and leaked via HTTP Referer headers, making them unsuitable for sensitive tokens. Fixes #6132 Human Coded --- app/api/notify/route.ts | 2 +- ...otification-management-token.empty-fallback.test.ts | 10 +++++----- lib/notification-management-token.ts | 7 ++----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/api/notify/route.ts b/app/api/notify/route.ts index f3121d984..5184181ca 100644 --- a/app/api/notify/route.ts +++ b/app/api/notify/route.ts @@ -321,7 +321,7 @@ export async function DELETE(req: NextRequest) { ); } - const providedManagementToken = getNotificationManagementToken(req, undefined, searchParams); + const providedManagementToken = getNotificationManagementToken(req); const authorization = await authorizeNotificationMutation( req, normalizedUsername, diff --git a/lib/notification-management-token.empty-fallback.test.ts b/lib/notification-management-token.empty-fallback.test.ts index 6b195f728..5114df148 100644 --- a/lib/notification-management-token.empty-fallback.test.ts +++ b/lib/notification-management-token.empty-fallback.test.ts @@ -68,16 +68,16 @@ describe('notification management token empty / missing inputs', () => { expect(getNotificationManagementToken(req, body)).toBe('body-token-xyz'); }); - it('extracts token from search params managementToken', () => { + it('ignores managementToken in query parameters (security: no URL leakage)', () => { const req = mockRequest(); const params = new URLSearchParams({ managementToken: 'query-token-123' }); - expect(getNotificationManagementToken(req, undefined, params)).toBe('query-token-123'); + expect(getNotificationManagementToken(req, undefined)).toBeNull(); }); - it('extracts token from search params token fallback', () => { + it('ignores token query parameter (security: no URL leakage)', () => { const req = mockRequest(); const params = new URLSearchParams({ token: 'fallback-token-456' }); - expect(getNotificationManagementToken(req, undefined, params)).toBe('fallback-token-456'); + expect(getNotificationManagementToken(req, undefined)).toBeNull(); }); it('prefers header over body when both are present', () => { @@ -90,7 +90,7 @@ describe('notification management token empty / missing inputs', () => { const req = mockRequest(); const body = { managementToken: 'body-value' }; const params = new URLSearchParams({ managementToken: 'query-value' }); - expect(getNotificationManagementToken(req, body, params)).toBe('body-value'); + expect(getNotificationManagementToken(req, body)).toBe('body-value'); }); it('ignores body managementToken when it is not a string', () => { diff --git a/lib/notification-management-token.ts b/lib/notification-management-token.ts index 9d3eda22b..fdbfad7c0 100644 --- a/lib/notification-management-token.ts +++ b/lib/notification-management-token.ts @@ -13,8 +13,7 @@ export function hashNotificationManagementToken(token: string): string { export function getNotificationManagementToken( request: Request, - body?: { managementToken?: unknown }, - searchParams?: URLSearchParams + body?: { managementToken?: unknown } ): string | null { const headerToken = request.headers.get('x-notification-token')?.trim(); if (headerToken) return headerToken; @@ -24,9 +23,7 @@ export function getNotificationManagementToken( return bodyToken.trim(); } - const queryToken = - searchParams?.get('managementToken')?.trim() || searchParams?.get('token')?.trim(); - return queryToken || null; + return null; } export function verifyNotificationManagementToken( From 308e6ba900c1dab9a760ef1f6a464774320535ec Mon Sep 17 00:00:00 2001 From: atul-upadhyay-7 Date: Sun, 21 Jun 2026 16:35:56 +0530 Subject: [PATCH 2/2] fix: update test to pass management token via header instead of query param The test for DELETE /api/notify was passing the management token via URL query parameter, which is no longer supported after the security fix. Updated the test to pass the token via x-notification-token header and extended makeRequest to accept custom headers. Fixes CI failure for PR #6229 --- app/api/notify/route.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/api/notify/route.test.ts b/app/api/notify/route.test.ts index f5de17961..130ee6a22 100644 --- a/app/api/notify/route.test.ts +++ b/app/api/notify/route.test.ts @@ -47,13 +47,19 @@ import { gitHubUserValidator } from '@/services/github/validate-user'; import { getClientIp } from '@/utils/getClientIp'; import { verifyGitHubOwner } from '@/lib/github-owner-verification'; -const makeRequest = (method: string, body?: object, search?: string) => { +const makeRequest = ( + method: string, + body?: object, + search?: string, + headers?: Record +) => { const url = `http://localhost:3000/api/notify${search ? '?' + search : ''}`; return new NextRequest(url, { method, headers: { 'x-forwarded-for': '127.0.0.1', Authorization: 'Bearer test-owner-token', + ...headers, }, body: body ? JSON.stringify(body) : undefined, }); @@ -593,7 +599,9 @@ describe('DELETE /api/notify', () => { vi.mocked(Notification.deleteOne).mockResolvedValue({ deletedCount: 1 } as never); const res = await DELETE( - makeRequest('DELETE', undefined, `user=testuser&managementToken=${managementToken}`) + makeRequest('DELETE', undefined, 'user=testuser', { + 'x-notification-token': managementToken, + }) ); const data = await res.json();