Next.js Security — CVE-2025-29927 اللي قلب الدنيا
Middleware bypass و Server Actions abuse و ISR poisoning و RSC leaks
ليه Next.js بيضاعف الأبواب اللي قدامه؟
Next.js مش framework واحد. ده 7 frameworks في صندوق واحد.
React frontend + Node backend + Edge middleware + RSC + Server Actions + Image Optimizer + ISR cache.
- بس استنى يا حضرتك.. أنا فاكره React بس!
متوقّع كالعادة يا مستجد. ده اللي بيخدع الكل. كل واحد من الـ 7 دول جبهة مستقلة بذاتها. وكل deployment فيه واحد منهم متفعّل بشكل غريب.
curl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" على /admin. اللي بيحصل فعلياً: الـ middleware كله بـ bypass — auth، rate limit، redirect، كله. آلاف التطبيقات اللي معتمدة على middleware للـ authorization اتفتحت في يوم واحد. الدرس: ما تعتمدش على middleware وحده. كل route يفحص الـ session بنفسه.CVE-2025-29927 — Middleware Authorization Bypass
أكبر CVE في تاريخ Next.js (مارس 2025). header داخلي اسمه x-middleware-subrequest اتستخدم لتعدية الـ middleware authorization كله.
# Vulnerable: Next.js < 14.2.25 / < 15.2.3 يفحص هذا header
# لمنع loops لكن لم يعقّمه من external requests
curl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" \
https://target.com/admin
# النتيجة: middleware يُتخطّى بالكامل
# auth checks في middleware.ts → bypassed
# rewrite/redirect → bypassed
# rate limit → bypassed// الحماية المعمّق — لا تعتمد على middleware وحده للـ authz
// كل route يفحص في handler:
export async function GET(req) {
const session = await getSession(req);
if (!session?.user?.isAdmin) return new Response('forbidden', { status: 403 });
// ...
}Server Actions — باب جديد قدامه كلياً
Server Actions = دوال بتتنادى من الـ client عن طريق POST مشفّر. بس:
- كل Server Action endpoint مفتوح للعامة — حتى لو مفيش UI بينديها. المهاجم بيعدّ الـ actions من الـ bundle ويناديهم مباشرة.
- الـ Authentication مش بتحصل تلقائي — لازم تفحصها يدوي في كل action.
- Action IDs ثابتة عبر الـ deploys (لو ما اتشفّروش). المهاجم بيحفظ الـ ID ويعيد استخدامه.
- FormData parsing — أنواع مش متوقعة (Files بتيجي كـ string).
// خطر — Server Action بدون auth
'use server';
export async function deleteUser(id: string) {
await db.user.delete({ where: { id } });
}
// أي مستخدم يستطيع استدعاءها (POST مع action ID)
// curl -X POST https://target.com/page \
// -H "Next-Action: 7f8a..." \
// -d '["userid"]'
// آمن
'use server';
export async function deleteUser(id: string) {
const session = await getServerSession(authOptions);
if (!session?.user?.isAdmin) throw new Error('unauthorized');
if (!validateId(id)) throw new Error('bad id');
await db.user.delete({ where: { id } });
}
// في next.config.js — تشفير action IDs
experimental: {
serverActions: { allowedOrigins: ['app.target.gov'] }
}React Server Components (RSC) — Data Leaks
// خطر — تمرير user object كامل إلى client component
// app/profile/page.tsx (Server Component)
export default async function Page() {
const user = await db.user.findUnique({ where: { id }, include: { ... } });
return <ProfileCard user={user} />; // user يحوي passwordHash, email...
}
// كل props إلى Client Component تُسلسل و تُرسل إلى المتصفح
// المهاجم يفتح DevTools → ينظر إلى __NEXT_DATA__ أو RSC payload و يقرأ كل شيء
// آمن — مرّر فقط ما يحتاج
return <ProfileCard
name={user.name}
avatar={user.avatar}
// لا passwordHash, mfaSecret, internal flags
/>;
// أو DTO صريح
const dto = pick(user, ['id', 'name', 'avatar']);
return <ProfileCard user={dto} />;API Routes / Route Handlers
// app/api/user/[id]/route.ts
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
// خطر بدون auth
return Response.json(await db.user.findUnique({ where: { id: params.id } }));
}
// IDOR: GET /api/user/123 ← أي user يقرأ أي user
// + DTO leak
// آمن
import { getServerSession } from 'next-auth';
export async function GET(req: NextRequest, { params }) {
const session = await getServerSession(authOptions);
if (!session) return new Response('unauthorized', { status: 401 });
// ownership
if (params.id !== session.user.id && !session.user.isAdmin) {
return new Response('forbidden', { status: 403 });
}
const user = await db.user.findUnique({
where: { id: params.id },
select: { id: true, name: true, email: true } // explicit fields
});
return Response.json(user);
}ISR / Cache Poisoning
Next.js بيـ cache صفحات وAPI responses بالـ URL. المهاجم يقدر يسمّم الـ cache بطلب هو متحكّم فيه.
# في app/products/[slug]/page.tsx
# revalidate = 3600
# المهاجم يستخدم header غير عادي يصل إلى logic:
GET /products/widget HTTP/1.1
X-Forwarded-Host: evil.com
# داخل الصفحة:
const url = headers().get('x-forwarded-host'); ← يستخدمه في canonical link
# النتيجة: cached version لـ /products/widget يحوي canonical = evil.com
# كل user لاحق يصل لنفس الصفحة المسمومة- متعتمدش على request headers وأنت بترسم cached pages.
- عرّف cache key بصراحة — متخليش Next يستنتج لوحده.
- اضبط الـ trust proxy headers.
- revalidateTag و revalidatePath لازم يطلبوا auth أو secret.
Image Optimizer SSRF
Next عنده endpoint اسمه /_next/image بيـ fetch أي URL عشان يحسّنه. لو الـ remotePatterns مفتوحة، الـ endpoint ده بيبقى SSRF proxy على طبق.
// next.config.js — خطر
images: {
domains: ['*'], // أو dangerouslyAllowSVG: true
remotePatterns: [{ protocol: 'https', hostname: '**' }]
}
// PoC — قراءة AWS metadata من خلف الخادم
GET /_next/image?url=http://169.254.169.254/latest/meta-data/&w=128&q=75
// SVG-based XSS لو dangerouslyAllowSVG: true بدون CSP
// (افتح SVG فيه <script>)
// آمن
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'cdn.target.gov' },
{ protocol: 'https', hostname: 'images.target.gov' }
],
// dangerouslyAllowSVG: false (افتراضي)
contentSecurityPolicy: "default-src 'self'; script-src 'none';"
}Open Redirect عبر next/router
// خطر
const router = useRouter();
router.push(searchParams.get('next')); // المهاجم يعيد توجيه
// PoC: /login?next=//evil.com → بعد login يذهب لـ evil.com
// أو next=javascript:alert(1)
// آمن
function safeNext(n: string) {
if (!n) return '/';
if (!n.startsWith('/') || n.startsWith('//')) return '/';
return n;
}
router.push(safeNext(searchParams.get('next')));Environment variables — تسرّبات شائعة
- NEXT_PUBLIC_* بتتحقن في bundle الـ client. متحطش secrets فيها أبداً.
- process.env في Server Component آمن، بس لو مرّرته لـ Client Component، خلاص اتنشر.
- .env.local في git — حطّه في .gitignore. استخدم secret manager في prod (Vercel env, AWS Secrets Manager).
- Build-time vs runtime — السرّ في NEXT_PUBLIC بيتحط وقت البناء، مش هيتغيّر من غير rebuild.
// server-only — Next مكتبة تمنع الاستيراد من client component
// app/lib/secret.ts
import 'server-only';
export const apiKey = process.env.API_KEY;
// لو client component استورد هذا، build يفشل. حماية compile-time.Authentication — Auth.js / Clerk pitfalls
// next-auth (Auth.js) — أخطاء شائعة
export const authOptions = {
providers: [...],
// خطر — لم يضبط secret
// secret: process.env.NEXTAUTH_SECRET, ← مطلوب في prod
// خطر — JWT بدون encryption
jwt: { encryption: false }, ← قبل v4 default خطر
callbacks: {
async jwt({ token, user }) {
if (user) token.role = user.role; // مدخل الـ role في JWT
return token;
},
async session({ session, token }) {
// خطر — تمرير token كاملاً للـ client
session.user = token; // يشمل internal flags
// آمن
session.user = { id: token.sub, role: token.role, name: token.name };
return session;
}
}
};
// تأكد أن middleware يفحص session لكل route محمي
export { default } from 'next-auth/middleware';
export const config = { matcher: ['/admin/:path*', '/api/admin/:path*'] };Edge Runtime vs Node Runtime
- Edge runtime بيشتغل على V8 isolates، مش Node كامل. بعض المكتبات (crypto, fs) مش هتشتغل.
- Edge أسرع، بس أقل قوة — مفيش DB queries طويلة.
- middleware بيشتغل على Edge افتراضياً. لو نقلته لـ Node، تأكد إن الـ routes لسه متطابقة.
- السرّيات في Edge runtime موجودة في كل region — التوزيع الجغرافي ممكن يخالف data residency rules عندك.
Headers و CSP في Next
// next.config.js
const securityHeaders = [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
},
};
// CSP مع nonce (متاح عبر middleware)
// middleware.ts
import { NextResponse } from 'next/server';
import { randomBytes } from 'crypto';
export function middleware(req) {
const nonce = randomBytes(16).toString('base64');
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
`.replace(/\s{2,}/g, ' ').trim();
const res = NextResponse.next();
res.headers.set('Content-Security-Policy', csp);
res.headers.set('x-nonce', nonce);
return res;
}checklist مراجعة Next.js app
- الإصدار >= 14.2.25 / 15.2.3 (CVE-2025-29927).
- الـ Middleware مش طبقة الـ authz الوحيدة — كل route يعيد الفحص.
- كل Server Action بيفحص session + input.
- RSC: ما تمررش entities كاملة لـ client component.
- API Routes: ownership check + select صريح.
- Image: remotePatterns صارمة.
- NEXT_SERVER_ACTIONS_ENCRYPTION_KEY مضبوط.
- Headers: HSTS, CSP, Permissions-Policy.
- NEXT_PUBLIC_* مفيهاش secrets.
- package server-only على ملفات السرّيات.
- Auth.js: secret + algorithms مضبوطين.
- open redirect filter في كل router.push بياخد user input.
غلطات الـ junior في Next
- auth في middleware بس — CVE-2025-29927 خرّب اللعبة دي. كل route لازم يفحص بنفسه.
- NEXT_PUBLIC_API_KEY — أي حاجة بـ NEXT_PUBLIC بتطلع في bundle الـ client. السر اللي حطّيته بقى public بمعنى الكلمة.
- Server Action من غير revalidation — الـ user يضغط Submit مرتين بسرعة، يطلع double charge. لازم idempotency.
- Server Action بياخد ID من client من غير ownership check — "هو الـ user مش هيغيرها" — هيغيّرها يا نجم. هيغيّرها بـ Burp.
- RSC بترجع entity كامل — السيرفر بيـ pass الـ user object للـ client component، فيه password_hash. الـ user بيـ inspect element.
- Image Optimizer مفتوح — remotePatterns: '**' = SSRF عبر
/_next/image?url=http://169.254.169.254.
الخلاصة الناشفة
Next مش "framework" — Next "platform". وكل platform بيدّيك سرعة بسعر معقّد.
السرعة في الـ DX. التعقيد في الحماية: 7 طبقات، كل واحدة لازم تتأمّن لوحدها.
اكتبها على ظهر إيدك:
كل route، كل Action، كل API. اوعى تثق في middleware. اوعى تثق في الـ client. ولا حتى في الـ session token من غير ما تتأكد منه على السيرفر.