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.
| Package | Affected versions | Patched versions |
|---|---|---|
happy-domnpm | < 20.8.9 | 20.8.9 |
Affected products
1Patches
168324c21d7b9fix: [#2117] Fixes issue related to cookies from the current origin being forwarded to the target origin in fetch requests (#2117)
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- github.com/capricorn86/happy-dom/blob/f8d8cad41e9722fab9eefb9dfb3cca696462e908/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.tsnvdPatchWEB
- github.com/capricorn86/happy-dom/commit/68324c21d7b98f53f7bb5a7b3e185bda7106e751nvdPatchWEB
- github.com/capricorn86/happy-dom/pull/2117nvdIssue TrackingPatchWEB
- github.com/capricorn86/happy-dom/security/advisories/GHSA-w4gp-fjgq-3q4gnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-w4gp-fjgq-3q4gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34226ghsaADVISORY
- github.com/capricorn86/happy-dom/releases/tag/v20.8.9nvdProductRelease NotesWEB
News mentions
0No linked articles in our index yet.