GitHub Security Advisory: GHSA-3rmj-9m5h-8fpv
Package: @astrojs/node (NPM)
Severity: MODERATE (CVSS 5.9)
CWE: CWE-770: Allocation of Resources Without Limits or Throttling
Vulnerable versions: < 10.0.0
Patched in: 10.0.0

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

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:

```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-s