VYPR
Medium severity4.3GHSA Advisory· Published May 11, 2026· Updated May 13, 2026

CVE-2026-42565

CVE-2026-42565

Description

@workos/authkit-session is a toolkit for building WorkOS AuthKit framework integrations. Prior to 0.5.1, an open redirect vulnerability exists in AuthService.handleCallback due to insufficient validation of the returnPathname value derived from the OAuth state parameter. The state parameter is round-tripped through the identity provider (IdP) and can be influenced by an attacker. The handleCallback function decodes and returns returnPathname without enforcing restrictions on origin or scheme. As a result, attacker-controlled values may be returned to the application. If this value is used directly in a redirect, it may cause the user to be redirected to an external, attacker-controlled site. This vulnerability is fixed in 0.5.1.

Affected products

1

Patches

1
f56e1d6214a9

Merge commit from fork

https://github.com/workos/authkit-sessionNick NisiApr 24, 2026via ghsa
3 files changed · +88 2
  • src/service/AuthService.ts+2 1 modified
    @@ -14,6 +14,7 @@ import type {
       SessionEncryption,
       SessionStorage,
     } from '../core/session/types.js';
    +import { sanitizeReturnPathname } from '../utils.js';
     
     /**
      * Merge two `HeadersBag` values. `Set-Cookie` matching is case-insensitive;
    @@ -383,7 +384,7 @@ export class AuthService<TRequest, TResponse> {
           return {
             response: clear.response ?? save.response,
             headers: mergeHeaderBags(save.headers, clear.headers),
    -        returnPathname: returnPathname ?? '/',
    +        returnPathname: sanitizeReturnPathname(returnPathname),
             state: customState,
             authResponse,
           };
    
  • src/utils.spec.ts+58 1 modified
    @@ -1,4 +1,4 @@
    -import { once } from './utils.js';
    +import { once, sanitizeReturnPathname } from './utils.js';
     
     describe('utils', () => {
       describe('once', () => {
    @@ -9,4 +9,61 @@ describe('utils', () => {
           expect(fn()).toEqual(1); // test it's the same on every call
         });
       });
    +
    +  describe('sanitizeReturnPathname (CWE-601 open-redirect protection)', () => {
    +    it.each([
    +      ['absolute URL to evil host', 'https://evil.com/steal'],
    +      ['absolute http URL', 'http://evil.com/steal'],
    +      ['protocol-relative URL', '//evil.com/steal'],
    +      ['backslash smuggle', '/\\evil.com/path'],
    +      ['double-backslash', '\\\\evil.com/path'],
    +      ['javascript: scheme', 'javascript:alert(1)'],
    +      ['data: scheme', 'data:text/html,<script>alert(1)</script>'],
    +      ['tab smuggling', '/\tevil.com'],
    +      ['newline smuggling', '/\nevil.com'],
    +      ['carriage-return smuggling', '/\revil.com'],
    +      ['url-encoded slashes', '%2f%2fevil.com/steal'],
    +      ['double-encoded slashes', '%252f%252fevil.com/steal'],
    +      ['userinfo smuggling', 'https://user:pass@evil.com/path'],
    +      ['leading-whitespace protocol-relative', '   //evil.com'],
    +      ['ftp scheme', 'ftp://evil.com'],
    +      ['null-byte injection', '/path%00.evil.com'],
    +    ])('neutralizes %s to a same-origin relative path', (_desc, payload) => {
    +      const result = sanitizeReturnPathname(payload);
    +      const resolved = new URL(result, 'https://trusted.example.com');
    +      expect(resolved.origin).toBe('https://trusted.example.com');
    +    });
    +
    +    it.each([
    +      ['/dashboard'],
    +      ['/dashboard?tab=settings'],
    +      ['/dashboard#billing'],
    +      ['/docs/api?v=2#auth'],
    +    ])('preserves legitimate path %s', path => {
    +      expect(sanitizeReturnPathname(path)).toBe(path);
    +    });
    +
    +    it.each([[undefined], [null], [''], [42], [{}]])(
    +      'falls back to `/` for %p',
    +      input => {
    +        expect(sanitizeReturnPathname(input)).toBe('/');
    +      },
    +    );
    +
    +    it('accepts a custom fallback', () => {
    +      expect(sanitizeReturnPathname(undefined, '/login')).toBe('/login');
    +    });
    +
    +    it('sanitizes an unsafe custom fallback — the fallback is not trusted either', () => {
    +      expect(sanitizeReturnPathname(undefined, '//evil.com')).toBe('/');
    +      expect(sanitizeReturnPathname(undefined, 'https://evil.com/x')).toBe(
    +        '/x',
    +      );
    +    });
    +
    +    it('backstops to `/` when both input and fallback are unusable', () => {
    +      expect(sanitizeReturnPathname(undefined, '')).toBe('/');
    +      expect(sanitizeReturnPathname(null, null as unknown as string)).toBe('/');
    +    });
    +  });
     });
    
  • src/utils.ts+28 0 modified
    @@ -41,3 +41,31 @@ export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
       for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;
       return diff === 0;
     }
    +
    +/**
    + * Normalize an untrusted return-path candidate (e.g. decoded from OAuth
    + * state) to a same-origin relative URL. The returned value always begins
    + * with exactly one `/`, safe to emit directly as a `Location` header.
    + *
    + * Parsing against a throwaway origin lets the WHATWG URL parser strip any
    + * smuggled host, scheme, backslash, tab, or newline; the leading-slash
    + * normalization defuses `//evil.com`-style protocol-relative redirects
    + * (CWE-601). `fallback` is sanitized by the same pipeline so a hostile
    + * fallback can't reopen the hole.
    + */
    +export function sanitizeReturnPathname(
    +  input: unknown,
    +  fallback: string = '/',
    +): string {
    +  for (const candidate of [input, fallback]) {
    +    if (typeof candidate !== 'string' || candidate.length === 0) continue;
    +    try {
    +      const parsed = new URL(candidate, 'https://placeholder.invalid');
    +      const path = '/' + parsed.pathname.replace(/^\/+/, '');
    +      return `${path}${parsed.search}${parsed.hash}`;
    +    } catch {
    +      // Unparseable; try the next candidate.
    +    }
    +  }
    +  return '/';
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.