CVE-2026-41067 — astro
MEDIUMCVE-2026-41067 is a medium-severity (CVSS 6.1) Cross-site Scripting (XSS) vulnerability in astro. A fix is available for astro — see the affected versions and patch details below.
Astro: XSS via incomplete `</script>` sanitization in `define:vars` allows case-insensitive and whitespace-based bypass
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-41067.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
CVE-2026-41067 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 377,636 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
astronpmDescription
Summary
The defineScriptVars function in Astro's server-side rendering pipeline uses a case-sensitive regex /<\/script>/g to sanitize values injected into inline <script> tags via the define:vars directive. HTML parsers close <script> elements case-insensitively and also accept whitespace or / before the closing >, allowing an attacker to bypass the sanitization with payloads like </Script>, </script >, or </script/> and inject arbitrary HTML/JavaScript.
Details
The vulnerable function is defineScriptVars at packages/astro/src/runtime/server/render/util.ts:42-53:
export function defineScriptVars(vars: Record<any, any>) {
let output = '';
for (const [key, value] of Object.entries(vars)) {
output += `const ${toIdent(key)} = ${JSON.stringify(value)?.replace(
/<\/script>/g, // ← Case-sensitive, exact match only
'\\x3C/script>',
)};\n`;
}
return markHTMLString(output);
}
This function is called from renderElement at util.ts:172-174 when a <script> element has define:vars:
if (name === 'script') {
delete props.hoist;
children = defineScriptVars(defineVars) + '\n' + children;
}
The regex /<\/script>/g fails to match three classes of closing script tags that HTML parsers accept per the HTML specification §13.2.6.4:
- Case variations:
</Script>,</SCRIPT>,</sCrIpT>— HTML tag names are case-insensitive but the regex has noiflag. - Whitespace before
>:</script >,</script\t>,</script\n>— after the tag name, the HTML tokenizer enters the "before attribute name" state on ASCII whitespace. - Self-closing slash:
</script/>— the tokenizer enters "self-closing start tag" state on/.
JSON.stringify() does not escape <, >, or / characters, so all these payloads pass through serialization unchanged.
Execution flow: User-controlled input (e.g., Astro.url.searchParams) → assigned to a variable → passed via define:vars on a <script> tag → renderElement → defineScriptVars → incomplete sanitization → injected into <script> block in HTML response → browser closes the script element early → attacker-controlled HTML parsed and executed.
PoC
Step 1: Create an SSR Astro page (src/pages/index.astro):
---
const name = Astro.url.searchParams.get('name') || 'World';
---
<html>
<body>
<h1>Hello</h1>
<script define:vars={{ name }}>
console.log(name);
</script>
</body>
</html>
Step 2: Ensure SSR is enabled in astro.config.mjs:
export default defineConfig({
output: 'server'
});
Step 3: Start the dev server and visit:
http://localhost:4321/?name=</Script><img/src=x%20onerror=alert(document.cookie)>
Step 4: View the HTML source. The output contains:
<script>const name = "</Script><img/src=x onerror=alert(document.cookie)>";
console.log(name);
</script>
The browser's HTML parser matches </Script> case-insensitively, closing the script block. The <img onerror=alert(document.cookie)> is then parsed as HTML and the JavaScript in onerror executes.
Alternative bypass payloads:
/?name=</script ><img/src=x onerror=alert(1)>
/?name=</script/><img/src=x onerror=alert(1)>
/?name=</SCRIPT><img/src=x onerror=alert(1)>
Impact
An attacker can execute arbitrary JavaScript in the context of a victim's browser session on any SSR Astro application that passes request-derived data to define:vars on a <script> tag. This is a documented and expected usage pattern in Astro.
Exploitation enables:
- Session hijacking via cookie theft (
document.cookie) - Credential theft by injecting fake login forms or keyloggers
- Defacement of the rendered page
- Redirection to attacker-controlled domains
The vulnerability affects all Astro versions that support define:vars and is exploitable in any SSR deployment where user input reaches a define:vars script variable.
Recommended Fix
Replace the case-sensitive exact-match regex with a comprehensive escape that covers all HTML parser edge cases. The simplest correct fix is to escape all < characters in the JSON output:
export function defineScriptVars(vars: Record<any, any>) {
let output = '';
for (const [key, value] of Object.entries(vars)) {
output += `const ${toIdent(key)} = ${JSON.stringify(value)?.replace(
/</g,
'\\u003c',
)};\n`;
}
return markHTMLString(output);
}
This is the standard approach used by frameworks like Next.js and Rails. Replacing every < with \u003c is safe inside JSON string contexts (JavaScript treats \u003c as < at runtime) and eliminates all possible </script> variants including case variations, whitespace, and self-closing forms.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | astro | all versions | 6.1.6npm install astro@6.1.6 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for astro, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update astro to 6.1.6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-41067 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-41067 can be triaged on real exposure rather than presence alone.
Tailored to CVE-2026-41067. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-41067 in your dependencies?
O3 Security finds CVE-2026-41067 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.