{"id":"GHSA-f48w-9m4c-m7f5","summary":"Astro: XSS via unescaped spread attribute names in renderHTMLElement (incomplete fix for CVE-2026-54298)","details":"## Summary\n\nThe fix for CVE-2026-54298 (GHSA-jrpj-wcv7-9fh9) added an `INVALID_ATTR_NAME_CHAR` guard to `addAttribute()` so that spread-prop attribute names containing `\"' \u003e/=` or whitespace are dropped. A second attribute-rendering path, `renderHTMLElement()` in `packages/astro/src/runtime/server/render/dom.ts`, has its own inline attribute loop that does not go through `addAttribute()` and was not updated. It interpolates the attribute name unescaped and only escapes the value, so untrusted prop keys spread onto a native-`HTMLElement`-subclass component can still break out of the attribute context, resulting in XSS.\n\n## Details\n\n`renderHTMLElement` builds attributes directly:\n\n```js\nfor (const attr in props) {\n  attrHTML += ` ${attr}=\"${toAttributeString(await props[attr])}\"`;\n}\n```\n\nThe attribute name (`attr`) is interpolated raw; only the value is escaped via `toAttributeString`. By contrast, the hardened `addAttribute` in `util.ts` rejects invalid names:\n\n```js\nif (INVALID_ATTR_NAME_CHAR.test(key)) { return ''; } // /[\\s\"'\u003e/=]/\n```\n\n`renderHTMLElement` is reached from `component.ts` when the component is a native `HTMLElement` subclass:\n\n```js\nif (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {\n  const output = await renderHTMLElement(result, Component, _props, slots);\n}\n```\n\nwhere `_props` carries spread props verbatim.\n\n### Reachability\n\nThe branch only runs when `typeof HTMLElement === 'function'` at SSR time. In default Node SSR `HTMLElement` is `undefined`, so the branch is dead. It becomes reachable when the SSR runtime exposes a global `HTMLElement` (Deno, Bun with a DOM shim, or jsdom/happy-dom in Node) **and** a class extending `HTMLElement` is used directly as an Astro component that receives untrusted-keyed spread props.\n\n## Proof of Concept\n\nGiven malicious spread props:\n\n```js\nconst maliciousProps = {\n  'onmouseover=alert(document.domain) x': 'y',\n  'x\u003e\u003cscript\u003ealert(1)\u003c/script\u003e': 'z',\n};\n```\n\n- `addAttribute` (post-fix) → `\u003cmy-el\u003e\u003c/my-el\u003e` (key stripped — safe)\n- `renderHTMLElement` → `\u003cmy-el onmouseover=alert(document.domain) x=\"y\" x\u003e\u003cscript\u003ealert(1)\u003c/script\u003e=\"z\"\u003e\u003c/my-el\u003e` (handler + `\u003cscript\u003e` injected — XSS)\n\nEquivalent Astro template, served by an SSR runtime that defines a global `HTMLElement`:\n\n```astro\n---\nimport MyElement from '../MyElement.js'; // class MyElement extends HTMLElement {}\nconst userInput = Astro.url.searchParams;  // untrusted keys\n---\n\u003cMyElement {...Object.fromEntries(userInput)} /\u003e\n```\n\n## Impact\n\nCross-site scripting (CWE-79) via attribute-name breakout — the same vulnerability class as CVE-2026-54298, in a code path its fix did not cover. An attacker who controls the keys of an object spread onto a native-`HTMLElement`-subclass component can inject arbitrary event-handler attributes or sibling elements (including `\u003cscript\u003e`) into the SSR output. Reachability is constrained by the runtime and component preconditions described above.","aliases":["CVE-2026-59729"],"modified":"2026-08-12T20:45:07.421317803Z","published":"2026-07-20T23:21:58Z","database_specific":{"github_reviewed_at":"2026-07-20T23:21:58Z","nvd_published_at":"2026-07-27T20:16:40Z","cwe_ids":["CWE-79"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/withastro/astro/security/advisories/GHSA-f48w-9m4c-m7f5"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-59729"},{"type":"WEB","url":"https://github.com/withastro/astro/pull/17251"},{"type":"WEB","url":"https://github.com/withastro/astro/commit/5240e26c9dd91f9bc7140dcfacdb48d5a132830d"},{"type":"PACKAGE","url":"https://github.com/withastro/astro"},{"type":"WEB","url":"https://github.com/withastro/astro/releases/tag/astro@7.0.6"}],"affected":[{"package":{"name":"astro","ecosystem":"npm","purl":"pkg:npm/astro"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"7.0.6"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-f48w-9m4c-m7f5/GHSA-f48w-9m4c-m7f5.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N"}]}