{"id":"GHSA-ff38-p3qj-4pmf","kind":"vulnerability","aliases":["GHSA-ff38-p3qj-4pmf"],"related":[],"title":"XSS via unescaped code for unsupported languages in @astrojs/prism","description":"## Summary\n\n`@astrojs/prism` did not escape code when the requested language was not supported by Prism. `runHighlighterWithAstro()` only HTML-escaped code through `Prism.highlight()` when a grammar was found. For any unsupported language, it returned the original code unchanged, and every caller inserted that result into the page as raw HTML.\n\nAs a result, a code block with an unsupported language label, including common labels such as `vue`, `svelte`, `mdx`, `console`, or `text`, was rendered as live HTML instead of displayed as code.\n\n## Details\n\nThe affected fallback is in `packages/astro-prism/src/highlighter.ts`:\n\n```ts\nconst grammar = Prism.languages[lang];\nlet html = code;\nif (grammar) {\n\thtml = Prism.highlight(code, grammar, lang);\n}\n```\n\nThe returned `html` is rendered without further escaping by:\n\n- The `<Prism />` component in `@astrojs/prism`, through `set:html`.\n- The `prism()` syntax highlighting extension in `@astrojs/markdoc` (`@astrojs/markdoc/prism`), for every code fence.\n\nSupported languages, and code blocks with no language (which default to `plaintext`), were escaped correctly.\n\n## Impact\n\nAffected applications could be vulnerable to stored cross-site scripting.\n\n- **Markdoc:** Markdoc does not render HTML by default (`allowHTML: false`). On sites that use the `prism()` extension, a content author could get around this by placing HTML inside a code fence with an unsupported language. The HTML and JavaScript would then be rendered and executed on the site's origin for every visitor. This matters for sites where Markdoc content is written by people who cannot otherwise add HTML or code to the site, such as CMS editors or outside contributors.\n- **`<Prism />` component:** Applications that pass untrusted input to the `code` prop together with an unsupported or user-controlled `lang` were vulnerable in the same way.\n\nMarkdown and MDX content highlighted with `markdown.syntaxHighlight: 'prism'` goes through the same fallback. This does not add new capability there, because Markdown and MDX already allow raw HTML.\n\n## Patches\n\nFixed in `@astrojs/prism` 4.1.2 and `@astrojs/markdoc` 2.1.2. Code is now escaped when no Prism grammar is available, so unsupported languages are displayed as plain text.\n\n## Workarounds\n\n- Use Shiki instead of Prism for syntax highlighting (the default for Markdown, and the `shiki()` extension for Markdoc).\n- Only pass languages that Prism supports to the `<Prism />` component, or HTML-escape untrusted `code` before passing it in.\n- For Markdoc content from less-trusted authors, avoid the `prism()` extension until a patched version is available.\n\n## Credit\n\nReported by [@oss-security-shopify](https://github.com/oss-security-shopify).","summary":"Code blocks with a language Prism does not support are inserted into the page as raw HTML instead of escaped text. An attacker who can write content, such as a CMS editor or outside contributor on a Markdoc site, can run scripts for every visitor.","severity":"medium","cvss":5.4,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"@astrojs/prism","fixed":["4.1.2"],"affected":"<= 4.1.1","ecosystem":"npm"},{"name":"@astrojs/markdoc","fixed":["2.1.2"],"affected":">= 0.3.0, <= 2.1.1","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["astro","cms","cwe-79","markdoc","package","prism","static-sites"],"not_affected_if":["shiki-highlighting","supported-languages-only","trusted-authors"],"how_to_check":"- Check the installed version of `@astrojs/prism` with `npm ls @astrojs/prism` and of `@astrojs/markdoc` with `npm ls @astrojs/markdoc`.\n- Look for uses of the `<Prism />` component with untrusted `code` or user-controlled `lang` props.\n- Look for the `prism()` extension in the Markdoc config.","fix":"- `@astrojs/prism`: `4.1.2`\n- `@astrojs/markdoc`: `2.1.2`","status":"open","published_at":"2026-10-28T19:27:39+00:00","first_seen_at":"2026-11-09T22:53:16+00:00","triaged_at":"2026-11-09T22:56:26+00:00","updated_at":"2026-11-09T22:56:26+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-11-09T22:56:26Z","model":"claude-haiku-5-5","output":{"fix":"- `@astrojs/prism`: `4.1.2`\n- `@astrojs/markdoc`: `2.1.2`","reach":"internet","title":"Astro Prism: unsupported code languages render as live HTML, enabling XSS","impact":"medium","summary":"Code blocks with a language Prism does not support are inserted into the page as raw HTML instead of escaped text. An attacker who can write content, such as a CMS editor or outside contributor on a Markdoc site, can run scripts for every visitor.","audience":"developers","category":"package","nicknames":[],"who_cares":["astro","markdoc","prism","static-sites","cms"],"confidence":0.85,"fix_status":"available","needs_auth":"unknown","worst_case":"Attacker script runs on the site's origin for every visitor.","analyst_note":"Product data gives both fixed versions; the affected Markdoc range is 0.3.0 to 2.1.1 with fix 2.1.2. Who can write Markdoc content on a typical site is not stated by the sources.","how_to_check":"- Check the installed version of `@astrojs/prism` with `npm ls @astrojs/prism` and of `@astrojs/markdoc` with `npm ls @astrojs/markdoc`.\n- Look for uses of the `<Prism />` component with untrusted `code` or user-controlled `lang` props.\n- Look for the `prism()` extension in the Markdoc config.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["shiki-highlighting","supported-languages-only","trusted-authors"],"who_is_affected":"Sites built with Astro that use the `<Prism />` component with untrusted or user-controlled `lang` values, or the `prism()` extension in Markdoc where content comes from less-trusted authors. Markdoc does not render HTML by default, but the Prism fallback bypasses that setting inside code fences. Sites using Shiki, Markdown-only content from trusted authors, or only supported Prism languages are not affected.","user_interaction":"yes","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.000916,"input_tokens":5544,"output_tokens":724,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-79':25B '/oss-security-shopify).':461C '2.1.2':377C '4.1.2':374C '4pmf':14A,19A 'add':303C,358C 'affect':161C,225C 'allow':367C 'allowhtml':244C 'alreadi':366C 'applic':226C,318C 'around':258C 'astro':22B 'astrojs/markdoc':21A,203C,376C 'astrojs/markdoc/prism':204C 'astrojs/prism':9A,20A,78C,193C,373C 'attack':54B 'author':255C,441C 'avail':387C,451C 'avoid':442C 'block':33B,132C,213C 'caller':117C 'cannot':301C 'capabl':360C 'cms':23B,62B,312C 'code':4A,32B,82C,97C,113C,131C,158C,173C,178C,207C,212C,265C,306C,325C,378C,429C 'common':139C 'compon':191C,317C,423C 'consol':146C 'const':167C 'content':58B,254C,295C,345C,436C 'contributor':66B,316C 'control':334C 'correct':223C 'could':227C,256C 'credit':452C 'cross':233C 'cross-sit':232C 'cwe':24B 'default':218C,243C,406C 'detail':159C 'display':156C,392C 'editor':63B,313C 'escap':51B,81C,96C,188C,222C,381C,427C 'everi':75B,116C,206C,287C 'execut':280C 'extens':201C,252C,412C,445C 'fallback':162C,354C 'fals':245C 'fenc':208C,266C 'ff38':12A,17A 'fix':371C 'found':104C 'get':257C 'ghsa':11A,16A 'ghsa-ff38-p3qj-4pmf':10A,15A 'github.com':460C 'github.com/oss-security-shopify).':459C 'goe':350C 'grammar':102C,168C,175C,179C,385C 'highlight':200C,346C,404C 'html':48B,95C,126C,153C,172C,176C,183C,196C,241C,262C,272C,304C,369C,426C 'html-escap':94C,425C 'impact':224C 'includ':138C 'input':322C 'insert':42B,118C 'insid':263C 'instead':49B,154C,399C 'javascript':274C 'label':137C,140C 'lang':170C,180C,335C 'languag':7A,36B,86C,108C,136C,210C,216C,270C,390C,417C 'less':439C 'less-trust':438C 'let':171C 'live':152C 'markdoc':26B,69B,236C,237C,294C,414C,435C 'markdown':342C,363C,408C 'markdown.syntaxhighlight':348C 'matter':290C 'mdx':145C,344C,365C 'new':359C 'origin':112C,285C 'oss':456C 'oss-security-shopifi':455C 'otherwis':302C 'outsid':65B,315C 'p3qj':13A,18A 'packag':27B 'packages/astro-prism/src/highlighter.ts':165C 'page':45B,123C 'pass':320C,416C,431C 'patch':370C,448C 'peopl':299C 'place':261C 'plain':394C 'plaintext':220C 'prism':28B,37B,91C,198C,251C,349C,384C,401C,419C,444C 'prism.highlight':99C,177C 'prism.languages':169C 'prop':326C 'raw':47B,125C,368C 'render':150C,185C,240C,278C 'report':453C 'request':85C 'result':120C,129C 'return':110C,182C 'run':72B 'runhighlighterwithastro':92C 'script':73B,235C 'secur':457C 'set':195C 'shiki':398C,411C 'shopifi':458C 'site':31B,70B,234C,247C,283C,292C,309C 'static':30B 'static-sit':29B 'store':231C 'summari':77C 'support':40B,89C,209C,420C 'svelt':144C 'syntax':199C,403C 'text':52B,148C,395C 'togeth':327C 'trust':440C 'ts':166C 'unchang':114C 'unescap':3A 'unsupport':6A,107C,135C,269C,330C,389C 'untrust':321C,428C 'use':249C,397C 'user':333C 'user-control':332C 'version':449C 'via':2A 'visitor':76B,288C 'vue':143C 'vulner':229C,337C 'way':341C 'without':186C 'workaround':396C 'would':275C 'write':57B 'written':297C 'xss':1A","nicknames":[],"mitigations":[],"who_affected":"Sites built with Astro that use the `<Prism />` component with untrusted or user-controlled `lang` values, or the `prism()` extension in Markdoc where content comes from less-trusted authors. Markdoc does not render HTML by default, but the Prism fallback bypasses that setting inside code fences. Sites using Shiki, Markdown-only content from trusted authors, or only supported Prism languages are not affected.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["markdoc","prism"],"ptokens":["astrojs","markdoc","prism"],"labels":["astro","cms","cwe79","markdoc","medium","npm","package","prism","staticsites"],"campaign_type":null,"search_a":"'4pmf':14A,19A 'astrojs/markdoc':21A 'astrojs/prism':9A,20A 'code':4A 'ff38':12A,17A 'ghsa':11A,16A 'ghsa-ff38-p3qj-4pmf':10A,15A 'languag':7A 'p3qj':13A,18A 'unescap':3A 'unsupport':6A 'via':2A 'xss':1A","search_b":"'-79':25B 'astro':22B 'attack':54B 'block':33B 'cms':23B,62B 'code':32B 'content':58B 'contributor':66B 'cwe':24B 'editor':63B 'escap':51B 'everi':75B 'html':48B 'insert':42B 'instead':49B 'languag':36B 'markdoc':26B,69B 'outsid':65B 'packag':27B 'page':45B 'prism':28B,37B 'raw':47B 'run':72B 'script':73B 'site':31B,70B 'static':30B 'static-sit':29B 'support':40B 'text':52B 'visitor':76B 'write':57B","search_c":"'/oss-security-shopify).':461C '2.1.2':377C '4.1.2':374C 'add':303C,358C 'affect':161C,225C 'allow':367C 'allowhtml':244C 'alreadi':366C 'applic':226C,318C 'around':258C 'astrojs/markdoc':203C,376C 'astrojs/markdoc/prism':204C 'astrojs/prism':78C,193C,373C 'author':255C,441C 'avail':387C,451C 'avoid':442C 'block':132C,213C 'caller':117C 'cannot':301C 'capabl':360C 'cms':312C 'code':82C,97C,113C,131C,158C,173C,178C,207C,212C,265C,306C,325C,378C,429C 'common':139C 'compon':191C,317C,423C 'consol':146C 'const':167C 'content':254C,295C,345C,436C 'contributor':316C 'control':334C 'correct':223C 'could':227C,256C 'credit':452C 'cross':233C 'cross-sit':232C 'default':218C,243C,406C 'detail':159C 'display':156C,392C 'editor':313C 'escap':81C,96C,188C,222C,381C,427C 'everi':116C,206C,287C 'execut':280C 'extens':201C,252C,412C,445C 'fallback':162C,354C 'fals':245C 'fenc':208C,266C 'fix':371C 'found':104C 'get':257C 'github.com':460C 'github.com/oss-security-shopify).':459C 'goe':350C 'grammar':102C,168C,175C,179C,385C 'highlight':200C,346C,404C 'html':95C,126C,153C,172C,176C,183C,196C,241C,262C,272C,304C,369C,426C 'html-escap':94C,425C 'impact':224C 'includ':138C 'input':322C 'insert':118C 'insid':263C 'instead':154C,399C 'javascript':274C 'label':137C,140C 'lang':170C,180C,335C 'languag':86C,108C,136C,210C,216C,270C,390C,417C 'less':439C 'less-trust':438C 'let':171C 'live':152C 'markdoc':236C,237C,294C,414C,435C 'markdown':342C,363C,408C 'markdown.syntaxhighlight':348C 'matter':290C 'mdx':145C,344C,365C 'new':359C 'origin':112C,285C 'oss':456C 'oss-security-shopifi':455C 'otherwis':302C 'outsid':315C 'packages/astro-prism/src/highlighter.ts':165C 'page':123C 'pass':320C,416C,431C 'patch':370C,448C 'peopl':299C 'place':261C 'plain':394C 'plaintext':220C 'prism':91C,198C,251C,349C,384C,401C,419C,444C 'prism.highlight':99C,177C 'prism.languages':169C 'prop':326C 'raw':125C,368C 'render':150C,185C,240C,278C 'report':453C 'request':85C 'result':120C,129C 'return':110C,182C 'runhighlighterwithastro':92C 'script':235C 'secur':457C 'set':195C 'shiki':398C,411C 'shopifi':458C 'site':234C,247C,283C,292C,309C 'store':231C 'summari':77C 'support':89C,209C,420C 'svelt':144C 'syntax':199C,403C 'text':148C,395C 'togeth':327C 'trust':440C 'ts':166C 'unchang':114C 'unsupport':107C,135C,269C,330C,389C 'untrust':321C,428C 'use':249C,397C 'user':333C 'user-control':332C 'version':449C 'visitor':288C 'vue':143C 'vulner':229C,337C 'way':341C 'without':186C 'workaround':396C 'would':275C 'written':297C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-10-28T19:27:39+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-ff38-p3qj-4pmf","url":"https://github.com/withastro/astro/security/advisories/GHSA-ff38-p3qj-4pmf","severity":"medium","published_at":"2026-10-28T19:27:39+00:00","modified_at":"2026-10-28T19:27:39+00:00","first_seen_at":"2026-11-09T22:53:16+00:00","fetched_at":"2026-11-09T22:53:16+00:00","note":null}],"documents":[{"id":110447,"url":"https://github.com/withastro/astro/security/advisories/GHSA-ff38-p3qj-4pmf","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}