From cca52bcd9ab86daee6df637dd3990d4719666326 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Fri, 24 Apr 2026 11:53:21 -0500 Subject: [PATCH] test: integration coverage for hostile returnPathname sanitization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an end-to-end `createAuthorization → handleCallback` round-trip against four CWE-601 payload classes (absolute URL, protocol-relative, backslash smuggle, javascript: scheme). Asserts the returned `returnPathname` starts with `/` and stays same-origin when resolved against a trusted base. The existing `utils.spec.ts` suite covers `sanitizeReturnPathname` in isolation; this test covers the wire connecting it to `handleCallback`. Without it, a future refactor that accidentally dropped the sanitizer call in AuthService.ts would still pass all 243 prior tests while reintroducing the vulnerability. --- src/service/AuthService.spec.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/service/AuthService.spec.ts b/src/service/AuthService.spec.ts index c69146c..d09003a 100644 --- a/src/service/AuthService.spec.ts +++ b/src/service/AuthService.spec.ts @@ -642,6 +642,39 @@ describe('AuthService', () => { expect(result.returnPathname).toBe('/'); }); + it.each([ + ['absolute URL', 'https://evil.com/steal'], + ['protocol-relative', '//evil.com/steal'], + ['backslash smuggle', '/\\evil.com'], + ['javascript: scheme', 'javascript:alert(1)'], + ])( + 'sanitizes a hostile returnPathname end-to-end: %s', + async (_label, hostilePathname) => { + const realStorage = makeStorage(); + const realService = new AuthService( + mockConfig as any, + realStorage as any, + makeClient() as any, + sessionEncryption, + ); + + const { cookieName } = await realService.createAuthorization('res', { + returnPathname: hostilePathname, + state: 'custom-state', + }); + const sealedState = realStorage.cookies.get(cookieName)!; + + const result = await realService.handleCallback('req', 'res', { + code: 'code', + state: sealedState, + }); + + expect(result.returnPathname.startsWith('/')).toBe(true); + const trusted = 'https://trusted.example.com'; + expect(new URL(result.returnPathname, trusted).origin).toBe(trusted); + }, + ); + it('best-effort clears the verifier cookie on OAuthStateMismatchError', async () => { const realStorage = makeStorage(); const realService = new AuthService(