الفرق الحمراء — الهجومخبير100mL31

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

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

#Next.js#Middleware#Server Actions#RSC#CVE-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 — أكبر breach في تاريخ Next
في مارس 2025، اتنشرت ثغرة في Next.js middleware. header اسمه x-middleware-subrequest كان موجود لمنع loops داخلية. المشكلة؟ مفيش validation إنه جاي من جوّه. attacker بيبعت 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 كله.

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
مين اتأثّر
أي تطبيق Next.js بيستخدم middleware للـ authorization. لو إصدارك < 14.2.25 أو < 15.2.3، رقّع دلوقتي. التحديث هو الحل الوحيد. WAF rules مجرد إسعافات أولية.
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'] }
}
Action ID enumeration
في dev mode، الـ action IDs قابلة للتعداد من الـ bundle. في prod، Next بيشفّرها بـ encryption key. ظبّط NEXT_SERVER_ACTIONS_ENCRYPTION_KEY بنفسك، وإلا كل deploy بيولّد key جديد ويكسر الـ clients اللي شغّالة من فترة.

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} />;
مهم
"use server" و "use client" حدود فعلية بين العالمين. اللي بيفضل على السيرفر هو بس اللي ما بتمررهوش لـ client component ولا بتـ return من server action. السرّيات تبقى في server-only files (مثلاً package server-only).

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 لاحق يصل لنفس الصفحة المسمومة
الحماية
  • متعتمدش على 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 على طبق.

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.
أدوات
next-secure-headers, Snyk, Semgrep p/nextjs, Vercel Firewall، وقواعد Cloudflare WAF للأنماط الخاصة بـ Next.

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

اللي بيحصل لما الـ 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 من غير ما تتأكد منه على السيرفر.