Skip to content

🚨 [security] Update astro 5.18.2 → 6.4.7 (major) - #444

Closed
depfu[bot] wants to merge 1 commit into
mainfrom
depfu/update/npm/astro-6.4.7
Closed

🚨 [security] Update astro 5.18.2 → 6.4.7 (major)#444
depfu[bot] wants to merge 1 commit into
mainfrom
depfu/update/npm/astro-6.4.7

Conversation

@depfu

@depfu depfu Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🚨 Your current dependencies have known security vulnerabilities 🚨

This dependency update fixes known security vulnerabilities. Please see the details below and assess their impact carefully. We recommend to merge and deploy this as soon as possible!


Here is everything you need to know about this upgrade. Please take a good look at what changed and the test results before merging this pull request.

What changed?

✳️ astro (5.18.2 → 6.4.7) · Repo · Changelog

Security Advisories 🚨

🚨 Astro: Reflected XSS via unescaped slot name

Summary

When a component uses a client:* directive, Astro inserts named slot content into a data-astro-template attribute without HTML escaping the slot name allowing an attacker to break out of the attribute context and inject arbitrary HTML, resulting in reflected XSS during SSR.

This is similar to GHSA-wrwg-2hg8-v723 but exploits a different injection point.

Vulnerable Code

packages/astro/src/runtime/server/render/component.ts:371:376

// component.ts:371
`<template data-astro-template${key !== 'default' ? `="${key}"` : ''}>${children[key]}</template>`

I found that key is interpolated directly into the attribute value without proper escaping.

Proof of Concept

For the PoC, I set up with a minimal repository with Astro 6.3.1, Node.js: v26.0.0.

astro.config.mjs

import react from '@astrojs/react';
import node from '@astrojs/node';
import { defineConfig } from 'astro/config';
export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
  integrations: [react()],
});

src/pages/index.astro

---
import Wrapper from '../components/Wrapper.jsx';
const slotName = Astro.url.searchParams.get('tab') ?? 'default';
---
<html><body>
  <Wrapper client:load>
    <div slot={slotName}>content</div>
  </Wrapper>
</body></html>

src/components/Wrapper.jsx

export default function Wrapper() { return null; }

Payload:

abc"></template></astro-island><img src=x onerror=confirm(document.domain)><!--

Accessing this URL will trigger the popup.

http://localhost:4321/?tab=abc%22%3E%3C%2Ftemplate%3E%3C%2Fastro-island%3E%3Cimg+src%3Dx+onerror%3Dconfirm(document.domain)%3E%3C!--

image

This will render in html.

<template data-astro-template="abc"></template></astro-island>
<img src=x onerror=confirm(document.domain)><!--">content</template>

Fix

I suggest leveraging the existing escape function on the slot name.

// component.ts:371
`<template data-astro-template${key !== 'default' ? `="${escapeHTML(String(key))}"` : ''}>${children[key]}</template>`

🚨 Astro: Host header SSRF in prerendered error page fetch

Summary

Astro SSR apps with prerendered error pages (/404 or /500 using export const prerender = true) fetch those pages over HTTP at runtime when an error occurs. The URL for this fetch is derived from request.url, which in turn gets its origin from the incoming Host header. When the Host header is not validated against allowedDomains, an attacker can point the fetch at an arbitrary host and read the response.

Who is affected

This affects SSR deployments that:

  1. Have a prerendered 404 or 500 page
  2. Use createRequestFromNodeRequest from astro/app/node with app.render() without overriding prerenderedErrorPageFetch — this includes custom servers built on the public API and third-party adapters

Not affected:

  • @astrojs/node >= 9.5.4 (reads error pages from disk)
  • @astrojs/cloudflare (uses the ASSETS binding)
  • The dev server (renders error pages in-process)

How it works

createRequestFromNodeRequest builds request.url from the raw Host / :authority header. The allowedDomains option is accepted but only gates X-Forwarded-For — it does not constrain the URL origin. (The public createRequest does fall back to localhost for unvalidated hosts; this internal builder did not.)

When app.render() encounters a 404 or 500 with a prerendered error route, default-handler.ts constructs the error page URL using the origin from request.url and fetches it via prerenderedErrorPageFetch, which defaults to global fetch. The response body is served to the client.

An attacker sends a request with Host: attacker-host:port, triggers an error (e.g., requesting a nonexistent path for a 404), and receives the response from the attacker-controlled host reflected back.

Remediation

The error page fetch origin is now validated against allowedDomains before use. When the host is validated, the original origin is preserved. Otherwise, it falls back to localhost. The fetch is also wrapped in a try/catch so that connection failures degrade gracefully to a plain error response.

Credit

5ud0 / Tarmo Technologies

🚨 Astro: XSS via Unescaped Attribute Names in Spread Props

Summary

The spreadAttributes function in Astro's server-side rendering pipeline iterates over object keys and passes them directly to addAttribute, which interpolates the key into the HTML output without escaping. When a developer uses the spread syntax {...props} on an HTML element and the object keys come from an untrusted source (API, CMS, URL parameters), an attacker can inject arbitrary HTML attributes including event handlers like onmousemove, onclick, or break out of the attribute context entirely to inject new elements.

Details

The vulnerable function is addAttribute at packages/astro/src/runtime/server/render/util.ts:81-141:

export function addAttribute(value: any, key: string, shouldEscape = true, tagName = '') {
    if (value == null) {
        return '';
    }
<span class="pl-k">return</span> <span class="pl-en">markHTMLString</span><span class="pl-kos">(</span><span class="pl-s">` <span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">key</span><span class="pl-kos">}</span></span>="<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-en">toAttributeString</span><span class="pl-kos">(</span><span class="pl-s1">value</span><span class="pl-kos">,</span> <span class="pl-s1">shouldEscape</span><span class="pl-kos">)</span><span class="pl-kos">}</span></span>"`</span><span class="pl-kos">)</span><span class="pl-kos">;</span> <span class="pl-c">//  key interpolated not escaped</span>

}

This function is called from spreadAttributes at packages/astro/src/runtime/server/index.ts:91-92:

for (const [key, value] of Object.entries(values)) {
    output += addAttribute(value, key, true, _name);
}

The toAttributeString function escapes the attribute value, but the attribute name key is never validated or escaped. An attacker can craft a JSON object with a key containing " characters to break out of the attribute context and inject event handlers.

Execution flow: User controlled object keys (from API, CMS, URL params) are spread onto element via {...props}. The compiler generates spreadAttributes(props) which iterates with Object.entries() and calls addAttribute(value, key). The key is interpolated as ` ${key}="${escapedValue}"`. A malicious key breaks attribute context, resulting in XSS.

POC

Create an SSR Astro page (src/pages/index.astro):

---
const props = JSON.parse(Astro.url.searchParams.get('props') || '{}');
---
<html>
<body>
  <h1>Hello</h1>
  <div {...props}>Move mouse here</div>
</body>
</html>

Enable SSR in astro.config.mjs (for URL based demo):

export default defineConfig({
  output: 'server'
});

Note: SSR is not required for the vulnerability to exist. In static builds (default), the attack vector is compromised data sources at build time (API, CMS, database). SSR simply makes the PoC easier to demonstrate via URL parameters.

Start the dev server and visit:

http://localhost:4321/?props={"x\" onmousemove=\"alert(document.cookie)\" y":""}

URL encoded:

http://localhost:4321/?props=%7B%22x%5C%22%20onmousemove%3D%5C%22alert(document.cookie)%5C%22%20y%22%3A%22%22%7D

View the HTML source. The output contains:

<div x" onmousemove="alert(document.cookie)" y="">Move mouse here</div>

The key x" onmousemove="alert(document.cookie)" y breaks out of the attribute context. Moving the mouse over the div executes the JavaScript.

Captura de tela 2026-06-02 005906

Impact

An attacker can execute arbitrary JavaScript in the context of a victim's browser session on any Astro application that spreads object props from untrusted sources onto HTML elements. This is a common pattern when integrating with external APIs or CMS systems. Exploitation enables session hijacking via cookie theft, credential theft by injecting fake login forms or keyloggers, defacement of the rendered page, and redirection to attacker controlled domains.

The vulnerability affects all Astro versions that support spread syntax on HTML elements and is exploitable in SSR, SSG (if build time data is compromised), and hybrid deployments.

🚨 Astro: Server island encrypted parameters vulnerable to cross-component replay

Impact

Astro versions prior to 6.1.10 used AES-GCM encryption to protect the confidentiality and integrity of server island props and slots parameters, but did not bind the ciphertext to its intended component or parameter type. An attacker could replay one component's encrypted props (p) value as another component's slots (s) value, or vice versa.

Since slots contain raw unescaped HTML while props may contain user-controlled values, this could lead to XSS in applications that meet all of the following conditions:

  • The application uses server islands
  • Two different server island components share the same key name for a prop and a slot
  • An attacker has full control over the value of the overlapping prop (requires a dynamically rendered page)

These conditions are very unlikely to occur in real-world production applications.

Patches

This has been patched in astro@6.1.10.

The fix binds each encrypted parameter to its target component and purpose using AES-GCM authenticated additional data (AAD). Each ciphertext now includes context like props:IslandName or slots:IslandName, so encrypted data for one component cannot be replayed against a different component, and encrypted props cannot be reused as slots.

References

🚨 Astro: XSS in define:vars via incomplete </script> tag sanitization

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:

  1. Case variations: </Script>, </SCRIPT>, </sCrIpT> — HTML tag names are case-insensitive but the regex has no i flag.
  2. Whitespace before >: </script >, </script\t>, </script\n> — after the tag name, the HTML tokenizer enters the "before attribute name" state on ASCII whitespace.
  3. 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 → renderElementdefineScriptVars → 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.

Release Notes

Too many releases to show here. View the full release notes.

↗️ @​astrojs/markdown-remark (indirect, 6.3.11 → 7.2.0) · Repo

Sorry, we couldn't find anything useful about this release.

↗️ @​astrojs/prism (indirect, 3.3.0 → 4.0.2) · Repo

Sorry, we couldn't find anything useful about this release.

↗️ @​astrojs/telemetry (indirect, 3.3.0 → 3.3.2) · Repo · Changelog

Release Notes

3.3.2 (from changelog)

More info than we can show here.

3.3.1 (from changelog)

More info than we can show here.

Does any of this look wrong? Please let us know.

↗️ @​shikijs/core (indirect, 3.23.0 → 4.2.0) · Repo

Release Notes

4.2.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ @​shikijs/langs (indirect, 3.23.0 → 4.2.0) · Repo

Release Notes

4.2.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ @​shikijs/themes (indirect, 3.23.0 → 4.2.0) · Repo

Release Notes

4.2.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ p-limit (indirect, 6.2.0 → 7.3.0) · Repo

Release Notes

7.3.0

More info than we can show here.

7.2.0

More info than we can show here.

7.1.1

More info than we can show here.

7.1.0

More info than we can show here.

7.0.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ p-queue (indirect, 8.1.1 → 9.3.0) · Repo

Release Notes

9.3.0

More info than we can show here.

9.2.0

More info than we can show here.

9.1.2

More info than we can show here.

9.1.1

More info than we can show here.

9.1.0

More info than we can show here.

9.0.1

More info than we can show here.

9.0.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ p-timeout (indirect, 6.1.4 → 7.0.1) · Repo

Release Notes

7.0.1

More info than we can show here.

7.0.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

↗️ shiki (indirect, 3.23.0 → 4.2.0) · Repo · Changelog

Release Notes

4.2.0

More info than we can show here.

4.1.0

More info than we can show here.

4.0.2

More info than we can show here.

4.0.1

More info than we can show here.

4.0.0

More info than we can show here.

Does any of this look wrong? Please let us know.

Commits

See the full diff on Github. The new version differs by more commits than we can show here.

🆕 @​clack/core (added, 1.4.1)

🆕 @​clack/prompts (added, 1.5.1)

🆕 @​shikijs/primitive (added, 4.2.0)

🆕 get-tsconfig (added, 5.0.0-beta.4)

🆕 resolve-pkg-maps (added, 1.0.0)

🆕 tinyclip (added, 0.1.14)

🆕 @​astrojs/compiler (added, 4.0.0)

🆕 @​astrojs/internal-helpers (added, 0.10.0)

🆕 is-docker (added, 4.0.0)

🆕 vite (added, 7.3.5)

🗑️ ansi-align (removed)

🗑️ tsconfck (removed)

🗑️ zod-to-ts (removed)

🗑️ base-64 (removed)

🗑️ boxen (removed)

🗑️ cli-boxes (removed)

🗑️ deterministic-object-hash (removed)

🗑️ import-meta-resolve (removed)

🗑️ yocto-spinner (removed)

🗑️ zod-to-json-schema (removed)

🗑️ typescript (removed)

🗑️ type-fest (removed)

🗑️ widest-line (removed)

🗑️ camelcase (removed)

🗑️ common-ancestor-path (removed)

🗑️ es-module-lexer (removed)


Depfu Status

Depfu will automatically keep this PR conflict-free, as long as you don't add any commits to this branch yourself. You can also trigger a rebase manually by commenting with @depfu rebase.

All Depfu comment commands
@​depfu rebase
Rebases against your default branch and redoes this update
@​depfu recreate
Recreates this PR, overwriting any edits that you've made to it
@​depfu merge
Merges this PR once your tests are passing and conflicts are resolved
@​depfu cancel merge
Cancels automatic merging of this PR
@​depfu close
Closes this PR and deletes the branch
@​depfu reopen
Restores the branch and reopens this PR (if it's closed)
@​depfu pause
Ignores all future updates for this dependency and closes this PR
@​depfu pause [minor|major]
Ignores all future minor/major updates for this dependency and closes this PR
@​depfu resume
Future versions of this dependency will create PRs again (leaves this PR as is)

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 16, 2026

Copy link
Copy Markdown

Deploying head-start with  Cloudflare Pages  Cloudflare Pages

Latest commit: 564ec27
Status:🚫  Build failed.

View logs

@depfu depfu Bot added the depfu label Jun 16, 2026
@depfu

depfu Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Closed in favor of #452.

@depfu depfu Bot closed this Jun 24, 2026
@depfu
depfu Bot deleted the depfu/update/npm/astro-6.4.7 branch June 24, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants