VYPR
High severity7.5NVD Advisory· Published Mar 27, 2026· Updated Apr 1, 2026

CVE-2026-34226

CVE-2026-34226

Description

Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. Versions prior to 20.8.9 may attach cookies from the current page origin (window.location) instead of the request target URL when fetch(..., { credentials: "include" }) is used. This can leak cookies from origin A to destination B. Version 20.8.9 fixes the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
happy-domnpm
< 20.8.920.8.9

Affected products

1

Patches

1
68324c21d7b9

fix: [#2117] Fixes issue related to cookies from the current origin being forwarded to the target origin in fetch requests (#2117)

https://github.com/capricorn86/happy-domDavid OrtnerMar 26, 2026via ghsa
4 files changed · +72 27
  • packages/happy-dom/src/cookie/CookieContainer.ts+1 1 modified
    @@ -63,7 +63,7 @@ export default class CookieContainer implements ICookieContainer {
     			if (
     				!CookieExpireUtility.hasExpired(cookie) &&
     				(!httpOnly || !cookie.httpOnly) &&
    -				(!url || CookieURLUtility.cookieMatchesURL(cookie, url || cookie.originURL))
    +				(!url || CookieURLUtility.cookieMatchesURL(cookie, url))
     			) {
     				cookies.push(cookie);
     			}
    
  • packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts+1 1 modified
    @@ -102,7 +102,7 @@ export default class FetchRequestHeaderUtility {
     			(options.request.credentials === 'same-origin' && !isCORS)
     		) {
     			const cookies = options.browserFrame.page.context.cookieContainer.getCookies(
    -				originURL,
    +				options.request[PropertySymbol.url],
     				false
     			);
     			if (cookies.length > 0) {
    
  • packages/happy-dom/test/fetch/Fetch.test.ts+29 10 modified
    @@ -19,6 +19,7 @@ import { afterEach, describe, it, expect, vi } from 'vitest';
     import FetchHTTPSCertificate from '../../src/fetch/certificate/FetchHTTPSCertificate.js';
     import * as PropertySymbol from '../../src/PropertySymbol.js';
     import { fail } from 'assert';
    +import Browser from '../../src/browser/Browser.js';
     
     const LAST_CHUNK = Buffer.from('0\r\n\r\n');
     
    @@ -1342,14 +1343,32 @@ describe('Fetch', () => {
     		});
     
     		it('Forwards "cookie", "authorization" or "www-authenticate" if request credentials are set to "include".', async () => {
    +			const browser = new Browser();
    +			const page = await browser.newPage();
    +			const browserFrame = page.mainFrame;
    +			const window = browserFrame.window;
    +
     			const originURL = 'https://localhost:8080';
    -			const window = new Window({ url: originURL });
    -			const url = 'https://other.origin.com/some/path';
    -			const cookies = 'key1=value1; key2=value2';
    +			const targetURL = 'https://other.origin.com/some/path';
     
    -			for (const cookie of cookies.split(';')) {
    -				window.document.cookie = cookie.trim();
    -			}
    +			browserFrame.url = originURL;
    +
    +			const sameOriginCookie = 'key1=value1';
    +			const crossOriginCookie = 'key2=value2';
    +
    +			window.document.cookie = sameOriginCookie;
    +
    +			browserFrame.page.context.cookieContainer.addCookies([
    +				{
    +					key: 'key2',
    +					originURL: new URL(targetURL),
    +					value: 'value2',
    +					domain: 'other.origin.com',
    +					path: '/',
    +					httpOnly: true,
    +					secure: true
    +				}
    +			]);
     
     			const network = mockNetwork('https', {
     				beforeResponse({ request, response }) {
    @@ -1358,7 +1377,7 @@ describe('Fetch', () => {
     				}
     			});
     
    -			await window.fetch(url, {
    +			await window.fetch(targetURL, {
     				headers: {
     					authorization: 'authorization',
     					'www-authenticate': 'www-authenticate'
    @@ -1368,7 +1387,7 @@ describe('Fetch', () => {
     
     			expect(network.requestHistory).toEqual([
     				{
    -					url,
    +					url: targetURL,
     					options: {
     						agent: false,
     						rejectUnauthorized: true,
    @@ -1388,7 +1407,7 @@ describe('Fetch', () => {
     					}
     				},
     				{
    -					url,
    +					url: targetURL,
     					options: {
     						method: 'GET',
     						headers: {
    @@ -1398,7 +1417,7 @@ describe('Fetch', () => {
     							'Accept-Encoding': 'gzip, deflate, br',
     							Origin: originURL,
     							Referer: originURL + '/',
    -							Cookie: cookies,
    +							Cookie: crossOriginCookie,
     							authorization: 'authorization',
     							'www-authenticate': 'www-authenticate'
     						},
    
  • packages/happy-dom/test/fetch/SyncFetch.test.ts+41 15 modified
    @@ -1717,20 +1717,31 @@ describe('SyncFetch', () => {
     
     		it('Forwards "cookie", "authorization" or "www-authenticate" if request credentials are set to "include".', () => {
     			const originURL = 'https://localhost:8080';
    +			const targetURL = 'https://other.origin.com/some/path';
     
     			browserFrame.url = originURL;
     
    -			const url = 'https://other.origin.com/some/path';
    -			const cookies = 'key1=value1; key2=value2';
    -			let requestArgs: string | null = null;
    +			const sameOriginCookie = 'key1=value1';
    +			const crossOriginCookie = 'key2=value2';
    +			const requestArgs: string[] = [];
     
    -			for (const cookie of cookies.split(';')) {
    -				window.document.cookie = cookie.trim();
    -			}
    +			window.document.cookie = sameOriginCookie;
    +
    +			browserFrame.page.context.cookieContainer.addCookies([
    +				{
    +					key: 'key2',
    +					originURL: new URL(targetURL),
    +					value: 'value2',
    +					domain: 'other.origin.com',
    +					path: '/',
    +					httpOnly: true,
    +					secure: true
    +				}
    +			]);
     
     			mockModule('child_process', {
     				execFileSync: (_command: string, args: string[]) => {
    -					requestArgs = args[1];
    +					requestArgs.push(args[1]);
     					return JSON.stringify({
     						error: null,
     						incomingMessage: {
    @@ -1748,19 +1759,34 @@ describe('SyncFetch', () => {
     			new SyncFetch({
     				browserFrame,
     				window,
    -				url,
    +				url: targetURL,
     				init: {
    +					credentials: 'include',
     					headers: {
     						authorization: 'authorization',
     						'www-authenticate': 'www-authenticate'
    -					},
    -					credentials: 'include'
    +					}
     				}
     			}).send();
     
    -			expect(requestArgs).toBe(
    +			expect(requestArgs).toEqual([
     				SyncFetchScriptBuilder.getScript({
    -					url: new URL(url),
    +					url: new URL(targetURL),
    +					method: 'OPTIONS',
    +					headers: {
    +						Accept: '*/*',
    +						'Accept-Encoding': 'gzip, deflate, br',
    +						'Access-Control-Request-Method': 'GET',
    +						'Access-Control-Request-Headers': 'authorization,www-authenticate',
    +						Connection: 'close',
    +						Origin: originURL,
    +						Referer: originURL + '/',
    +						'User-Agent': window.navigator.userAgent
    +					},
    +					body: null
    +				}),
    +				SyncFetchScriptBuilder.getScript({
    +					url: new URL(targetURL),
     					method: 'GET',
     					headers: {
     						Accept: '*/*',
    @@ -1769,16 +1795,16 @@ describe('SyncFetch', () => {
     						'Accept-Encoding': 'gzip, deflate, br',
     						Origin: originURL,
     						Referer: originURL + '/',
    -						Cookie: cookies,
    +						Cookie: crossOriginCookie,
     						authorization: 'authorization',
     						'www-authenticate': 'www-authenticate'
     					},
     					body: null
     				})
    -			);
    +			]);
     		});
     
    -		it('Sets document cookie string if the response contains a "Set-Cookie" header if request cridentials are set to "include".', () => {
    +		it('Sets document cookie string if the response contains a "Set-Cookie" header if request credentials are set to "include".', () => {
     			browserFrame.url = 'https://localhost:8080/';
     
     			mockModule('child_process', {
    

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

7

News mentions

0

No linked articles in our index yet.