Skip to content

fix(deps): update dependency astro to v7 [security]#80

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-astro-vulnerability
Open

fix(deps): update dependency astro to v7 [security]#80
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-astro-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
astro (source) ^6.0.0^7.0.0 age confidence

Astro: Reflected XSS via unescaped slot name

CVE-2026-50146 / GHSA-8hv8-536x-4wqp

More information

Details

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 '@&#8203;astrojs/react';
import node from '@&#8203;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>`

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Astro: Host header SSRF in prerendered error page fetch

CVE-2026-54299 / GHSA-2pvr-wf23-7pc7

More information

Details

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

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Astro: XSS via Unescaped Attribute Names in Spread Props

CVE-2026-54298 / GHSA-jrpj-wcv7-9fh9

More information

Details

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 '';
    }
    
    return markHTMLString(` ${key}="${toAttributeString(value, shouldEscape)}"`); //  key interpolated not escaped
}

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.

Severity

  • CVSS Score: 4.2 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Astro: Reflected XSS via unescaped View Transition animation properties

GHSA-4g3v-8h47-v7g6

More information

Details

Summary

Astro's server-side View Transition CSS generator interpolates animation properties into an inline <style> element without escaping them for the CSS and HTML contexts.

An attacker-controlled value passed to an animation property such as duration can contain a </style> sequence, terminate the generated style element, and inject arbitrary HTML or JavaScript.

This is similar to GHSA-8hv8-536x-4wqp, but exploits a different injection point: unescaped View Transition animation values in a server-generated <style> element rather than an unescaped slot name in a hydration template.

Like GHSA-8hv8-536x-4wqp, exploitation requires an application to pass attacker-controlled data to an Astro API. However, the value is subsequently inserted into the HTML response without context-appropriate escaping by Astro.

Details

packages/astro/src/runtime/server/transition.ts

The generated stylesheet is wrapped in a <style> element and marked as HTML-safe:

const css = sheet.toString();
result._metadata.extraHead.push(markHTMLString(`<style>${css}</style>`));

Animation properties are added to the stylesheet without escaping:

if (anim.duration) {
  addAnimationProperty(builder, 'animation-duration', toTimeValue(anim.duration));
}

For string values, toTimeValue() returns the input unchanged:

export function toTimeValue(num: number | string) {
  return typeof num === 'number' ? num + 'ms' : num;
}

As a result, a duration value containing </style> can escape from the generated style element.

Other TransitionAnimation properties, including easing, direction, delay, fillMode, and name, are serialized by the same animation builder. The following PoC only relies on the official fade() helper and its duration option.

PoC

Using:

  • astro@7.0.9
  • @astrojs/node@11.0.2
astro.config.mjs
import node from '@&#8203;astrojs/node';
import { defineConfig } from 'astro/config';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});
src/pages/index.astro
---
import { fade } from 'astro:transitions';

const duration = Astro.url.searchParams.get('duration') ?? '300ms';
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>PoC</title>
  </head>
  <body>
    <div transition:animate={fade({ duration })}>
      Animated content
    </div>
  </body>
</html>
Payload:

open:

http://localhost:4321/?duration=%3C%2Fstyle%3E%3Cscript%3Ealert(1)%3C%2Fscript%3E%3C!--

The browser interprets </style> as the end of the generated style element and executes the injected script. An alert dialog is displayed when the page is opened.

image
Impact

An attacker who can control a View Transition animation value can execute arbitrary JavaScript in the origin of the affected Astro application.

The query-based reflected XSS scenario affects on-demand/server-rendered routes, such as:

  • projects configured with output: "server";
  • pages using export const prerender = false;
  • other server-side data flows that pass attacker-controlled values into a View Transition animation definition.

Successful exploitation may allow access to sensitive page data and authenticated actions available to the victim.

Suggested Fix

Animation values should be serialized using context-appropriate CSS escaping or validation before being added to the generated stylesheet.

Additionally, content inserted into a raw <style> element must not be able to contain an HTML end-tag sequence such as </style>. The final generated CSS should be made safe for the HTML raw-text context before it is passed to markHTMLString().

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Astro: Cross-site scripting via unescaped transition:* directive values on hydrated islands

CVE-2026-59727 / GHSA-7pw4-f3q4-r2p2

More information

Details

Summary

When a transition:persist, transition:scope, or transition:persist-props directive is applied to a client-hydrated (client:*) component, Astro copied the directive value onto the rendered <astro-island> element without HTML-escaping it. If a developer reflects attacker-controlled input into one of these directives, an attacker can break out of the attribute and inject arbitrary HTML/JavaScript into the server-rendered output, resulting in reflected cross-site scripting (XSS).

Severity

Although a generic reflected XSS scores in the Medium range, exploitation here requires the application developer to have written a non-idiomatic pattern — passing untrusted, request-derived input directly into a transition directive. Astro applications that do not route untrusted input into these directives are unaffected. This mitigating precondition places the real-world severity at Low.

Details

In generateHydrateScript() (packages/astro/src/runtime/server/hydration.ts), every island property is HTML-escaped before serialization — the attrs, props, and opts assignments all pass through escapeHTML(). The transition directives, however, were copied verbatim:

transitionDirectivesToCopyOnIsland.forEach((name) => {
  if (typeof props[name] !== 'undefined') {
    island.props[name] = props[name]; // not escaped
  }
});

The <astro-island> element is serialized via renderElement('astro-island', island, false) with shouldEscape=false, and toAttributeString() returns the value unchanged in that mode. As a result there is no downstream re-escaping, and the raw directive value reaches the HTML response. This is the same output sink previously addressed for slot names in GHSA-8hv8-536x-4wqp.

The affected directives are:

  • data-astro-transition-scope (transition:scope)
  • data-astro-transition-persist (transition:persist)
  • data-astro-transition-persist-props (transition:persist-props)

Note that transition:persist is typed boolean | string, so passing a string value is a supported use of the API.

Proof of Concept

A component that reflects a query parameter into a transition directive:

---
const persist = Astro.url.searchParams.get('persist') ?? 'default';
---
<Island client:load transition:persist={persist} />

Request:

https://example.com/?persist="><img src=x onerror=alert(document.domain)>

Rendered output (before the fix):

<astro-island  data-astro-transition-persist=""><img src=x onerror=alert(document.domain)>></astro-island>

The " closes the attribute and the injected <img onerror=…> executes in the victim's browser.

Impact

Reflected XSS. An attacker who can induce a victim to visit a crafted URL can execute arbitrary script in the victim's session on the origin, subject to the requirement that the target application reflects untrusted input into one of the affected transition directives.

Affected Versions

astro >= 3.10.0, < 7.0.4 (introduced in 3.10.0, PR #​7861).

Patched Versions

astro >= 7.0.4. Fixed in PR #​17212 by HTML-escaping transition directive values before they are rendered onto the island element.

Workarounds

Do not pass untrusted or request-derived input into transition:persist, transition:scope, or transition:persist-props. If such input is required, HTML-escape or strictly validate it before passing it to the directive. Upgrading to astro@7.0.4 or later removes the need for manual mitigation.

Credits

Reported by @​jlgore.

Severity

  • CVSS Score: 2.1 / 10 (Low)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Astro: XSS via unescaped spread attribute names in renderHTMLElement (incomplete fix for CVE-2026-54298)

CVE-2026-59729 / GHSA-f48w-9m4c-m7f5

More information

Details

Summary

The fix for CVE-2026-54298 (GHSA-jrpj-wcv7-9fh9) added an INVALID_ATTR_NAME_CHAR guard to addAttribute() so that spread-prop attribute names containing "' >/= or whitespace are dropped. A second attribute-rendering path, renderHTMLElement() in packages/astro/src/runtime/server/render/dom.ts, has its own inline attribute loop that does not go through addAttribute() and was not updated. It interpolates the attribute name unescaped and only escapes the value, so untrusted prop keys spread onto a native-HTMLElement-subclass component can still break out of the attribute context, resulting in XSS.

Details

renderHTMLElement builds attributes directly:

for (const attr in props) {
  attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`;
}

The attribute name (attr) is interpolated raw; only the value is escaped via toAttributeString. By contrast, the hardened addAttribute in util.ts rejects invalid names:

if (INVALID_ATTR_NAME_CHAR.test(key)) { return ''; } // /[\s"'>/=]/

renderHTMLElement is reached from component.ts when the component is a native HTMLElement subclass:

if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
  const output = await renderHTMLElement(result, Component, _props, slots);
}

where _props carries spread props verbatim.

Reachability

The branch only runs when typeof HTMLElement === 'function' at SSR time. In default Node SSR HTMLElement is undefined, so the branch is dead. It becomes reachable when the SSR runtime exposes a global HTMLElement (Deno, Bun with a DOM shim, or jsdom/happy-dom in Node) and a class extending HTMLElement is used directly as an Astro component that receives untrusted-keyed spread props.

Proof of Concept

Given malicious spread props:

const maliciousProps = {
  'onmouseover=alert(document.domain) x': 'y',
  'x><script>alert(1)</script>': 'z',
};
  • addAttribute (post-fix) → <my-el></my-el> (key stripped — safe)
  • renderHTMLElement<my-el onmouseover=alert(document.domain) x="y" x><script>alert(1)</script>="z"></my-el> (handler + <script> injected — XSS)

Equivalent Astro template, served by an SSR runtime that defines a global HTMLElement:

---
import MyElement from '../MyElement.js'; // class MyElement extends HTMLElement {}
const userInput = Astro.url.searchParams;  // untrusted keys
---
<MyElement {...Object.fromEntries(userInput)} />
Impact

Cross-site scripting (CWE-79) via attribute-name breakout — the same vulnerability class as CVE-2026-54298, in a code path its fix did not cover. An attacker who controls the keys of an object spread onto a native-HTMLElement-subclass component can inject arbitrary event-handler attributes or sibling elements (including <script>) into the SSR output. Reachability is constrained by the runtime and component preconditions described above.

Severity

  • CVSS Score: 5.1 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Astro: Reflected XSS via unescaped slot name

CVE-2026-50146 / GHSA-8hv8-536x-4wqp

More information

Details

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 '@&#8203;astrojs/react';
import node from '@&#8203;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>`

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Astro: Host header SSRF in prerendered error page fetch

CVE-2026-54299 / GHSA-2pvr-wf23-7pc7

More information

Details

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

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:L/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Astro: XSS via Unescaped Attribute Names in Spread Props

CVE-2026-54298 / GHSA-jrpj-wcv7-9fh9

More information

Details

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 '';
    }
    
    return markHTMLString(` ${key}="${toAttributeString(value, shouldEscape)}"`); //  key interpolated not escaped
}

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.

Severity

  • CVSS Score: 4.2 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Astro: Cross-site scripting via unescaped transition:* directive values on hydrated islands

CVE-2026-59727 / GHSA-7pw4-f3q4-r2p2

More information

Details

Summary

When a transition:persist, transition:scope, or transition:persist-props directive is applied to a client-hydrated (client:*) component, Astro copied the directive value onto the rendered <astro-island> element without HTML-escaping it. If a developer reflects attacker-controlled input into one of these directives, an attacker can break out of the attribute and inject arbitrary HTML/JavaScript into the server-rendered output, resulting in reflected cross-site scripting (XSS).

Severity

Although a generic reflected XSS scores in the Medium range, exploitation here requires the application developer to have written a non-idiomatic pattern — passing untrusted, request-derived input directly into a transition directive. Astro applications that do not route untrusted input into these directives are unaffected. This mitigating precondition places the real-world severity at Low.

Details

In generateHydrateScript() (packages/astro/src/runtime/server/hydration.ts), every island property is HTML-escaped before serialization — the attrs, props, and opts assignments all pass through escapeHTML(). The transition directives, however, were copied verbatim:

transitionDirectivesToCopyOnIsland.forEach((name) => {
  if (typeof props[name] !== 'undefined') {
    island.props[name] = props[name]; // not escaped
  }
});

The <astro-island> element is serialized via renderElement('astro-island', island, false) with shouldEscape=false, and toAttributeString() returns the value unchanged in that mode. As a result there is no downstream re-escaping, and the raw directive value reaches the HTML response. This is the same output sink previously addressed for slot names in GHSA-8hv8-536x-4wqp.

The affected directives are:

  • data-astro-transition-scope (transition:scope)
  • data-astro-transition-persist (transition:persist)
  • data-astro-transition-persist-props (transition:persist-props)

Note that transition:persist is typed boolean | string, so passing a string value is a supported use of the API.

Proof of Concept

A component that reflects a query parameter into a transition directive:

---
const persist = Astro.url.searchParams.get('persist') ?? 'default';
---
<Island client:load transition:persist={persist} />

Request:

https://example.com/?persist="><img src=x onerror=alert(document.domain)>

Rendered output (before the fix):

<astro-island  data-astro-transition-persist=""><img src=x onerror=alert(document.domain)>></astro-island>

The " closes the attribute and the injected <img onerror=…> executes in the victim's browser.

Impact

Reflected XSS. An attacker who can induce a victim to visit a crafted URL can execute arbitrary script in the victim's session on the origin, subject to the requirement that the target application reflects untrusted input into one of the affected transition directives.

Affected Versions

astro >= 3.10.0, < 7.0.4 (introduced in 3.10.0, PR #​7861).

Patched Versions

astro >= 7.0.4. Fixed in PR #​17212 by HTML-escaping transition directive values before they are rendered onto the island element.

Workarounds

Do not pass untrusted or request-derived input into transition:persist, transition:scope, or transition:persist-props. If such input is required, HTML-escape or strictly validate it before passing it to the directive. Upgrading to astro@7.0.4 or later removes the need for manual mitigation.

Credits

Reported by @​jlgore.

Severity

  • CVSS Score: 2.1 / 10 (Low)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Astro: XSS via unescaped spread attribute names in renderHTMLElement (incomplete fix for CVE-2026-54298)

CVE-2026-59729 / GHSA-f48w-9m4c-m7f5

More information

Details

Summary

The fix for CVE-2026-54298 (GHSA-jrpj-wcv7-9fh9) added an INVALID_ATTR_NAME_CHAR guard to addAttribute() so that spread-prop attribute names containing "' >/= or whitespace are dropped. A second attribute-rendering path, renderHTMLElement() in packages/astro/src/runtime/server/render/dom.ts, has its own inline attribute loop that does not go through addAttribute() and was not updated. It interpolates the attribute name unescaped and only escapes the value, so untrusted prop keys spread onto a native-HTMLElement-subclass component can still break out of the attribute context, resulting in XSS.

Details

renderHTMLElement builds attributes directly:

for (const attr in props) {
  attrHTML += ` ${attr}="${toAttributeString(await props[attr])}"`;
}

The attribute name (attr) is interpolated raw; only the value is escaped via toAttributeString. By contrast, the hardened addAttribute in util.ts rejects invalid names:

if (INVALID_ATTR_NAME_CHAR.test(key)) { return ''; } // /[\s"'>/=]/

renderHTMLElement is reached from component.ts when the component is a native HTMLElement subclass:

if (!renderer && typeof HTMLElement === 'function' && componentIsHTMLElement(Component)) {
  const output = await renderHTMLElement(result, Component, _props, slots);
}

where _props carries spread props verbatim.

Reachability

The branch only runs when typeof HTMLElement === 'function' at SSR time. In default Node SSR HTMLElement is undefined, so the branch is dead. It becomes reachable when the SSR runtime exposes a global HTMLElement (Deno, Bun with a DOM shim, or jsdom/happy-dom in Node) and a class extending HTMLElement is used directly as an Astro component that receives untrusted-keyed spread props.

Proof of Concept

Given malicious spread props:

const maliciousProps = {
  'onmouseover=alert(document.domain) x': 'y',
  'x><script>alert(1)</script>': 'z',
};
  • addAttribute (post-fix) → <my-el></my-el> (key stripped — safe)
  • renderHTMLElement<my-el onmouseover=alert(document.domain) x="y" x><script>alert(1)</script>="z"></my-el> (handler + <script> injected — XSS)

Equivalent Astro template, served by an SSR runtime that defines a global HTMLElement:

---
import MyElement from '../MyElement.js'; // class MyElement extends HTMLElement {}
const userInput = Astro.url.searchParams;  // untrusted keys
---
<MyElement {...Object.fromEntries(userInput)} />
Impact

Cross-site scripting (CWE-79) via attribute-name breakout — the same vulnerability class as CVE-2026-54298, in a code path its fix did not cover. An attacker who controls the keys of an object spread onto a native-HTMLElement-subclass component can inject arbitrary event-handler attributes or sibling elements (including <script>) into the SSR output. Reachability is constrained by the runtime and component preconditions described above.

Severity

  • CVSS Score: 5.1 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Astro: Reflected XSS via unescaped View Transition animation properties

GHSA-4g3v-8h47-v7g6

More information

Details

Summary

Astro's server-side View Transition CSS generator interpolates animation properties into an inline <style> element without escaping them for the CSS and HTML contexts.

An attacker-controlled value passed to an animation property such as duration can contain a </style> sequence, terminate the generated style element, and inject arbitrary HTML or JavaScript.

This is similar to GHSA-8hv8-536x-4wqp, but exploits a different injection point: unescaped View Transition animation values in a server-generated <style> element rather than an unescaped slot name in a hydration template.

Like GHSA-8hv8-536x-4wqp, exploitation requires an application to pass attacker-controlled data to an Astro API. However, the value is subsequently inserted into the HTML response without context-appropriate escaping by Astro.

Details

packages/astro/src/runtime/server/transition.ts

The generated stylesheet is wrapped in a <style> element and marked as HTML-safe:

const css = sheet.toString();
result._metadata.extraHead.push(markHTMLString(`<style>${css}</style>`));

Animation properties are added to the stylesheet without escaping:

if (anim.duration) {
  addAnimationProperty(builder, 'animation-duration', toTimeValue(anim.duration));
}

For string values, toTimeValue() returns the input unchanged:

export function toTimeValue(num: number | string) {
  return typeof num === 'number' ? num + 'ms' : num;
}

As a result, a duration value containing </style> can escape from the generated style element.

Other TransitionAnimation properties, including easing, direction, delay, fillMode, and name, are serialized by the same animation builder. The following PoC only relies on the official fade() helper and its duration option.

PoC

Using:

  • astro@7.0.9
  • @astrojs/node@11.0.2
astro.config.mjs
import node from '@&#8203;astrojs/node';
import { defineConfig } from 'astro/config';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});
src/pages/index.astro
---
import { fade } from 'astro:transitions';

const duration = Astro.url.searchParams.get('duration') ?? '300ms';
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>PoC</title>
  </head>
  <body>
    <div transition:animate={fade({ duration })}>
      Animated content
    </div>
  </body>
</html>
Payload:

open:

http://localhost:4321/?duration=%3C%2Fstyle%3E%3Cscript%3Ealert(1)%3C%2Fscript%3E%3C!--

The browser interprets </style> as the end of the generated style element and executes the injected script. An alert dialog is displayed when the page is opened.

image
Impact

An attacker who can control a View Transition animation value can execute arbitrary JavaScript in the origin of the affected Astro application.

The query-based reflected XSS scenario affects on-demand/server-rendered routes, such as:

  • projects configured with output: "server";
  • pages using export const prerender = false;
  • other server-side data flows that pass attacker-controlled values into a View Transition animation definition.

Successful exploitation may allow access to sensitive page data and authenticated actions available to the victim.

Suggested Fix

Animation values should be serialized using context-appropriate CSS escaping or validation before being added to the generated stylesheet.

Additionally, content inserted into a raw <style> element must not be able to contain an HTML end-tag sequence such as </style>. The final generated CSS should be made safe for the HTML raw-text context before it is passed to markHTMLString().

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

withastro/astro (astro)

v7.1.0

Compare Source

Minor Changes
  • #​17302 5f4dc03 Thanks @​astrobot-houston! - Adds a new deferRender option to the glob() content loader

    When set to true, renderable entries (such as Markdown) are not rendered during content sync. Instead, rendering is deferred until the entry is actually rendered in a page, using the same on-demand path that .mdx files already use.

    This reduces memory usage during astro build for large collections whose rendered output is much larger than the source — for example, Markdown that uses heavy rehype plugins like rehype-katex. Such builds could previously run out of memory while storing the eagerly-rendered HTML for every entry.

    // src/content.config.ts
    import { defineCollection } from 'astro:content';
    import { glob } from 'astro/loaders';
    
    const docs = defineCollection({
      loader: glob({ pattern: '**/*.md', base: 'src/content/docs', deferRender: true }),
    });

    By default deferRender is false, preserving the existing behavior of rendering entries eagerly during sync so their rendered HTML can be cached across builds.

  • #​17296 30698a2 Thanks @​ematipico! - Adds a new experimental collectionStorage option for controlling how the content layer persists its data store

    By default, Astro serializes the entire content layer data store to a single file (.astro/data-store.json). For very large content collections, this file can grow large enough to hit platform file-size limits.

    Set experimental.collectionStorage: 'chunked' to instead split the data store across many smaller, content-addressed files inside a .astro/data-store/ directory, described by a manifest:

    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    
    export default defineConfig({
      experimental: {
        collectionStorage: 'chunked',
      },
    });

    Because each part file is named by a hash of its contents, unchanged parts keep the same name across builds and are not rewritten, and identical parts are deduplicated. The default value is 'single-file', which preserves the current behavior.

  • #​17214 44c4989 Thanks @​ematipico! - Adds support for the more specific CSP directives script-src-elem, script-src-attr, style-src-elem, and style-src-attr through a new kind option.

    Previously, CSP was only scoped to generic script-src/style-src directives. Now each source or hash can be scoped to a narrower directive — for example, to allow inline style attributes (such as those from define:vars or Shiki) without loosening the policy for your <style> and <link> elements.

Scoping sources and hashes in your config

Each entry in resources and hashes can be an object with a kind property. Depending on whether you use scriptDirective or styleDirective, "element" targets script-src-elem or style-src-elem, "attribute" targets script-src-attr or style-src-attr, and "default" (the same as a bare string or hash) targets script-src or style-src.

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  security: {
    csp: {
      scriptDirective: {
        resources: [{ resource: 'https://cdn.example.com', kind: 'element' }],
      },
      styleDirective: {
        resources: [{ resource: "'unsafe-inline'", kind: 'attribute' }],
      },
    },
  },
});
Scoping at runtime

The same kind option is available on the runtime CSP API, where the existing methods now also accept an object:

ctx.csp.insertScriptResource({ resource: 'https://cdn.example.com', kind: 'element' });
ctx.csp.insertStyleResource({ resource: "'unsafe-inline'", kind: 'attribute' });

Note

PR body was truncated to here.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added the security label Jul 15, 2026
@renovate
renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 2 times, most recently from bdff2f7 to 9ddead7 Compare July 20, 2026 21:48
@renovate renovate Bot changed the title chore(deps): update dependency astro to v6.4.6 [security] chore(deps): update dependency astro to v6.4.6 [security] - autoclosed Jul 21, 2026
@renovate renovate Bot closed this Jul 21, 2026
@renovate
renovate Bot deleted the renovate/npm-astro-vulnerability branch July 21, 2026 02:55
@renovate renovate Bot changed the title chore(deps): update dependency astro to v6.4.6 [security] - autoclosed fix(deps): update dependency astro to v7 [security] Jul 24, 2026
@renovate renovate Bot reopened this Jul 24, 2026
@renovate
renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 2 times, most recently from 9ddead7 to a958762 Compare July 24, 2026 22:54
@wiz-inc-38d59fb8d7

wiz-inc-38d59fb8d7 Bot commented Jul 24, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings -
Software Management Finding Software Management Findings -
Total -

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.

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