Skip to content

🚨 [security] Update astro 5.18.2 → 6.4.8 (major) - #452

Merged
MarleenEliza merged 3 commits into
mainfrom
depfu/update/npm/astro-6.4.8
Jul 28, 2026
Merged

🚨 [security] Update astro 5.18.2 → 6.4.8 (major)#452
MarleenEliza merged 3 commits into
mainfrom
depfu/update/npm/astro-6.4.8

Conversation

@depfu

@depfu depfu Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Upgrade to Astro 6

Supersedes #456 — this upgrade already bumps @astrojs/cloudflare to 13.7.0
(the security fix in #456) as part of the Astro 6 major upgrade, so #456 can
be closed once this merges.

Bumps Astro to ^6.4.8 and migrates the codebase per the official v5 → v6 guide. The initial dependency bump broke CI (ERESOLVE peer conflict); this PR carries the full set of migration changes needed to get green.

Changes

Dependencies

  • @astrojs/cloudflare ^12.6.7^13.7.0 — v13 is the release that peers astro@^6 (this was the root cause of the failing npm ci).

Content collections

  • Moved src/content/config.tssrc/content.config.ts (v6 required location) and updated importers (src/lib/content/*). Collections already used the Content Layer loader API, so no data-layer migration was needed.

Config (astro.config.ts)

  • Moved fonts out of experimental (now a stable top-level option).
  • Removed the platformProxy adapter option (removed in adapter v13; the Cloudflare Vite plugin handles local bindings automatically).
  • Skip the Cloudflare adapter under Vitest — v13's Vite plugin rejects the Node resolve.external list Astro sets on the SSR env, which otherwise breaks getViteConfig in tests.
  • Load .env via loadEnv into the astro:env defaults so local npm run build works from .env alone.
    Otherwise:
image

Cloudflare / Workers

  • wrangler.toml main@astrojs/cloudflare/entrypoints/server (v13 entrypoint).
  • Fixed the postbuild readme cleanup path for the new dist/client/ static output layout.

Service worker (dev)

  • Serve /service-worker.js in dev via a Vite dev-server middleware instead of an on-demand route. Since v13 runs dev routes in Cloudflare's workerd runtime, running esbuild inside the request handler crashed with __filename is not defined. Vite middleware runs in Node, so esbuild works there. Production (astro:build:done) is unchanged.

Verification

  • npm ci resolves cleanly
    -lint:eslint, lint:astro (0 errors)
  • test:unit — 243 passed
  • npm run build completes; prerenders all routes
  • lint:html — 0 errors
  • wrangler deploy --dry-run — bindings resolve, assets read from dist/client
  • astro dev — pages + /service-worker.js serve correctly

🚨 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.8) · 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.

↗️ smol-toml (indirect, 1.6.1 → 1.7.0) · Repo

Release Notes

1.7.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.2)

🆕 @​clack/prompts (added, 1.6.0)

🆕 @​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.15)

🆕 @​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)

@depfu depfu Bot added the depfu label Jun 24, 2026
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying head-start with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6e1fca0
Status:🚫  Build failed.

View logs

@MarleenEliza
MarleenEliza changed the base branch from main to feat/migrate-to-workers-cloudflare July 3, 2026 13:03
Base automatically changed from feat/migrate-to-workers-cloudflare to main July 22, 2026 14:02
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 22, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
head-start fe8df88 Commit Preview URL

Branch Preview URL
Jul 23 2026, 12:28 PM

- Bump astro to ^6.4.8 and @astrojs/cloudflare to ^13.7.0 (v6 peer)
- Move content config to src/content.config.ts (v6 required location)
- Move fonts out of experimental (now stable) in astro.config
- Drop removed platformProxy adapter option; skip adapter under Vitest
  (v13 Cloudflare Vite plugin rejects Astro's SSR resolve.external)
- Point wrangler main at @astrojs/cloudflare/entrypoints/server (v13)
- Fix postbuild readme cleanup path for new dist/client output layout
- Serve dev service worker via Vite middleware instead of a route, since
  v13 runs dev routes in workerd where esbuild's __filename is undefined
@MarleenEliza
MarleenEliza merged commit 19f14b7 into main Jul 28, 2026
5 checks passed
@MarleenEliza
MarleenEliza deleted the depfu/update/npm/astro-6.4.8 branch July 28, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants