اعرف اللي قصادك. احمي أرضك.
الفرق الحمراء — الهجوم

Next.js Security — CVE-2025-29927 اللي قلب الدنيا

Middleware bypass و Server Actions abuse و ISR poisoning و RSC leaks

المستوى
خبير
المدة
100m
الترتيب
الدرس 31 من 85
Next.jsMiddlewareServer ActionsRSCCVE-2025-29927

ليه Next.js بيضاعف الأبواب اللي قدامه؟

Next.js مش framework واحد. ده 7 frameworks في صندوق واحد.

React frontend + Node backend + Edge middleware + RSC + Server Actions + Image Optimizer + ISR cache.

- بس استنى يا حضرتك.. أنا فاكره React بس!

متوقّع كالعادة يا مستجد. ده اللي بيخدع الكل. كل واحد من الـ 7 دول جبهة مستقلة بذاتها. وكل deployment فيه واحد منهم متفعّل بشكل غريب.

تشبيه يقرّب الفكرة
تخيّل مبنى بـ 7 أدوار وفيه 7 أسانسيرات مختلفة (App Router، Pages، API، Edge، Server Actions، Image، ISR). مش كل أسانسير بيوصل كل دور. ولا كل أبوابه بتقفل بنفس الطريقة. المهاجم برسم خريطة الأسانسيرات (المصاعد) الأول، وبعدين بيختار سكته. إنت كـ developer شايف الـ feature بتاعتك. هو شايف 7 مداخل.

CVE-2025-29927 — Middleware Authorization Bypass

أكبر CVE في تاريخ Next.js (مارس 2025). header داخلي اسمه x-middleware-subrequest اتستخدم لتعدية الـ middleware authorization كله.

text
# 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
javascript
// الحماية المعمّق — لا تعتمد على 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).
javascript
// خطر — 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

tsx
// خطر — تمرير 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

typescript
// 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 بطلب هو متحكّم فيه.

text
# في 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 لاحق يصل لنفس الصفحة المسمومة

Image Optimizer SSRF

Next عنده endpoint اسمه /_next/image بيـ fetch أي URL عشان يحسّنه. لو الـ remotePatterns مفتوحة، الـ endpoint ده بيبقى SSRF proxy على طبق.

javascript
// 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

javascript
// خطر
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.
javascript
// 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

typescript
// 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

javascript
// 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

  1. الإصدار >= 14.2.25 / 15.2.3 (CVE-2025-29927).
  2. الـ Middleware مش طبقة الـ authz الوحيدة — كل route يعيد الفحص.
  3. كل Server Action بيفحص session + input.
  4. RSC: ما تمررش entities كاملة لـ client component.
  5. API Routes: ownership check + select صريح.
  6. Image: remotePatterns صارمة.
  7. NEXT_SERVER_ACTIONS_ENCRYPTION_KEY مضبوط.
  8. Headers: HSTS, CSP, Permissions-Policy.
  9. NEXT_PUBLIC_* مفيهاش secrets.
  10. package server-only على ملفات السرّيات.
  11. Auth.js: secret + algorithms مضبوطين.
  12. open redirect filter في كل router.push بياخد user input.

غلطات الـ junior في Next

الخلاصة الناشفة

Next مش "framework" — Next "platform". وكل platform بيدّيك سرعة بسعر معقّد.

السرعة في الـ DX. التعقيد في الحماية: 7 طبقات، كل واحدة لازم تتأمّن لوحدها.

اكتبها على ظهر إيدك:

كل route، كل Action، كل API. اوعى تثق في middleware. اوعى تثق في الـ client. ولا حتى في الـ session token من غير ما تتأكد منه على السيرفر.

خلّصت الدرس؟

علّمه علشان تفضل عارف انت فين على السكة.