إطار Next.js

أمان Next.js

32 دقيقة الدرس 34 من 40

مقدمة إلى أمان Next.js

الأمان هو جانب حاسم في تطوير تطبيقات الويب. يوفر Next.js ميزات أمان مدمجة، لكن يجب على المطورين تنفيذ تدابير إضافية لحماية التطبيقات من الثغرات الشائعة مثل CSRF و XSS وحقن SQL وهجمات DDoS.

عقلية الأمان: الأمان ليس ميزة تضيفها في النهاية—يجب دمجه في كل طبقة من تطبيقك منذ البداية.

الحماية من التزوير عبر المواقع (CSRF)

تخدع هجمات CSRF المستخدمين المصادق عليهم لتنفيذ إجراءات غير مرغوب فيها. لا يتضمن Next.js حماية CSRF مدمجة، لذلك يجب عليك تنفيذها يدويًا.

تنفيذ رموز CSRF

// lib/csrf.ts
import { randomBytes } from 'crypto';
import { cookies } from 'next/headers';

export function generateCSRFToken(): string {
  return randomBytes(32).toString('hex');
}

export function setCSRFToken(): string {
  const token = generateCSRFToken();
  cookies().set('csrf-token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 // 24 ساعة
  });
  return token;
}

export function verifyCSRFToken(token: string): boolean {
  const storedToken = cookies().get('csrf-token')?.value;
  return storedToken === token;
}

وسيط CSRF

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const PROTECTED_METHODS = ['POST', 'PUT', 'DELETE', 'PATCH'];

export function middleware(request: NextRequest) {
  // تخطي فحص CSRF للطرق الآمنة ومسارات API من نفس الأصل
  if (!PROTECTED_METHODS.includes(request.method)) {
    return NextResponse.next();
  }

  const csrfToken = request.headers.get('x-csrf-token');
  const csrfCookie = request.cookies.get('csrf-token')?.value;

  if (!csrfToken || !csrfCookie || csrfToken !== csrfCookie) {
    return NextResponse.json(
      { error: 'رمز CSRF غير صالح' },
      { status: 403 }
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/:path*', '/actions/:path*']
};

استخدام CSRF في النماذج

// app/contact/page.tsx
import { setCSRFToken } from '@/lib/csrf';

export default function ContactPage() {
  const csrfToken = setCSRFToken();

  return (
    <form action="/api/contact" method="POST">
      <input type="hidden" name="csrf_token" value={csrfToken} />
      <input type="text" name="name" placeholder="اسمك" />
      <textarea name="message" placeholder="رسالتك"></textarea>
      <button type="submit">إرسال</button>
    </form>
  );
}
// fetch من جانب العميل مع CSRF
'use client';

async function submitForm(data: FormData) {
  const csrfToken = document.cookie
    .split('; ')
    .find(row => row.startsWith('csrf-token='))
    ?.split('=')[1];

  const response = await fetch('/api/contact', {
    method: 'POST',
    headers: {
      'X-CSRF-Token': csrfToken || ''
    },
    body: data
  });

  return response.json();
}

نصيحة: بالنسبة إلى Server Actions في Next.js 14+، استخدم خطاف useFormState مع رموز CSRF المخفية أو نفذ التحقق من الأصل.

منع البرمجة النصية عبر المواقع (XSS)

تحقن هجمات XSS نصوصًا ضارة في تطبيقك. يهرب Next.js المحتوى تلقائيًا، لكن يجب أن تكون حذرًا مع سيناريوهات معينة.

الأنماط الخطرة التي يجب تجنبها

// ❌ خطير: استخدام dangerouslySetInnerHTML
function BlogPost({ content }: { content: string }) {
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

// ❌ خطير: حقن النص المباشر
function Analytics({ code }: { code: string }) {
  return <script>{code}</script>;
}

عرض المحتوى الآمن

// ✅ آمن: استخدم مكتبة التعقيم
import DOMPurify from 'isomorphic-dompurify';

function BlogPost({ content }: { content: string }) {
  const sanitizedContent = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'h1', 'h2', 'h3'],
    ALLOWED_ATTR: ['href', 'target']
  });

  return <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />;
}
// ✅ آمن: استخدم محلل markdown مع التعقيم
import { remark } from 'remark';
import html from 'remark-html';
import sanitizeHtml from 'sanitize-html';

async function parseMarkdown(markdown: string) {
  const result = await remark().use(html).process(markdown);
  return sanitizeHtml(result.toString());
}

التحقق من صحة المدخلات والتعقيم

// lib/validation.ts
import { z } from 'zod';

export const userInputSchema = z.object({
  name: z.string().min(2).max(50).regex(/^[a-zA-Z\s]+$/),
  email: z.string().email(),
  message: z.string().min(10).max(1000),
  website: z.string().url().optional()
});

// الاستخدام في مسار API
export async function POST(request: Request) {
  const body = await request.json();

  try {
    const validatedData = userInputSchema.parse(body);
    // معالجة البيانات المتحقق منها
    return Response.json({ success: true });
  } catch (error) {
    return Response.json(
      { error: 'إدخال غير صالح' },
      { status: 400 }
    );
  }
}

تحذير: لا تثق أبدًا في مدخلات المستخدم. تحقق دائمًا من صحة البيانات وعقمها من جانب الخادم، حتى لو كان لديك التحقق من جانب العميل.

سياسة أمان المحتوى (CSP)

CSP هي ميزة أمان قوية تساعد في منع هجمات XSS من خلال التحكم في الموارد التي يمكن تحميلها.

تنفيذ رؤوس CSP

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: `
      default-src 'self';
      script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.jsdelivr.net;
      style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
      img-src 'self' data: https://images.unsplash.com;
      font-src 'self' https://fonts.gstatic.com;
      connect-src 'self' https://api.example.com;
      frame-ancestors 'none';
      base-uri 'self';
      form-action 'self';
    `.replace(/\s{2,}/g, ' ').trim()
  }
];

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders
      }
    ];
  }
};

CSP المستند إلى Nonce (أكثر أمانًا)

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { randomBytes } from 'crypto';

export function middleware(request: NextRequest) {
  const nonce = randomBytes(16).toString('base64');

  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' data: https:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
  `.replace(/\s{2,}/g, ' ').trim();

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', cspHeader);
  response.headers.set('X-Nonce', nonce);

  return response;
}
// app/layout.tsx
import { headers } from 'next/headers';

export default function RootLayout({ children }) {
  const nonce = headers().get('X-Nonce') || '';

  return (
    <html>
      <head>
        <script nonce={nonce} src="/analytics.js"></script>
      </head>
      <body>{children}</body>
    </html>
  );
}

تحديد المعدل

احمِ واجهات برمجة التطبيقات الخاصة بك من الإساءة وهجمات DDoS باستخدام تحديد المعدل.

محدد معدل بسيط في الذاكرة

// lib/rate-limiter.ts
const rateLimit = new Map<string, { count: number; resetTime: number }>();

export function checkRateLimit(
  identifier: string,
  limit: number = 10,
  windowMs: number = 60000
): { allowed: boolean; remaining: number } {
  const now = Date.now();
  const userLimit = rateLimit.get(identifier);

  if (!userLimit || now > userLimit.resetTime) {
    rateLimit.set(identifier, {
      count: 1,
      resetTime: now + windowMs
    });
    return { allowed: true, remaining: limit - 1 };
  }

  if (userLimit.count >= limit) {
    return { allowed: false, remaining: 0 };
  }

  userLimit.count++;
  return { allowed: true, remaining: limit - userLimit.count };
}
// app/api/login/route.ts
import { checkRateLimit } from '@/lib/rate-limiter';

export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown';
  const { allowed, remaining } = checkRateLimit(ip, 5, 60000); // 5 طلبات في الدقيقة

  if (!allowed) {
    return Response.json(
      { error: 'طلبات كثيرة جدًا. يرجى المحاولة مرة أخرى لاحقًا.' },
      {
        status: 429,
        headers: {
          'Retry-After': '60',
          'X-RateLimit-Remaining': '0'
        }
      }
    );
  }

  // معالجة تسجيل الدخول
  return Response.json({ success: true }, {
    headers: {
      'X-RateLimit-Remaining': remaining.toString()
    }
  });
}

محدد معدل قائم على Redis (للإنتاج)

// lib/redis-rate-limiter.ts
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL!);

export async function checkRateLimitRedis(
  key: string,
  limit: number,
  windowSeconds: number
): Promise<{ allowed: boolean; remaining: number }> {
  const multi = redis.multi();

  multi.incr(key);
  multi.expire(key, windowSeconds);

  const results = await multi.exec();
  const count = results?.[0]?.[1] as number;

  if (count > limit) {
    return { allowed: false, remaining: 0 };
  }

  return { allowed: true, remaining: limit - count };
}

نصيحة: نفذ حدود معدل مختلفة لنقاط نهاية مختلفة. يجب أن يكون لنقاط نهاية المصادقة حدود أكثر صرامة من نقاط نهاية API العامة.

رؤوس الأمان

رؤوس الأمان الأساسية

// next.config.js
const securityHeaders = [
  {
    key: 'X-DNS-Prefetch-Control',
    value: 'on'
  },
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'
  },
  {
    key: 'X-Frame-Options',
    value: 'SAMEORIGIN'
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  },
  {
    key: 'X-XSS-Protection',
    value: '1; mode=block'
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin'
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()'
  }
];

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders
      }
    ];
  }
};

منع حقن SQL

استخدم الاستعلامات المعلمية وORMs لمنع هجمات حقن SQL.

استعلامات قاعدة البيانات الآمنة

// ❌ خطير: استيفاء السلسلة
async function getUser(email: string) {
  const query = `SELECT * FROM users WHERE email = '${email}'`;
  return db.execute(query);
}

// ✅ آمن: استعلام معلمي
async function getUser(email: string) {
  return db.query('SELECT * FROM users WHERE email = $1', [email]);
}

// ✅ آمن: استخدام Prisma ORM
async function getUser(email: string) {
  return prisma.user.findUnique({
    where: { email }
  });
}

أفضل ممارسات المصادقة

تجزئة كلمة المرور الآمنة

// lib/password.ts
import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12;

export async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}

export async function verifyPassword(
  password: string,
  hash: string
): Promise<boolean> {
  return bcrypt.compare(password, hash);
}

إدارة الجلسة الآمنة

// lib/session.ts
import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';

const secret = new TextEncoder().encode(process.env.JWT_SECRET!);

export async function createSession(userId: string) {
  const token = await new SignJWT({ userId })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('24h')
    .setIssuedAt()
    .sign(secret);

  cookies().set('session', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 // 24 ساعة
  });
}

export async function verifySession() {
  const token = cookies().get('session')?.value;
  if (!token) return null;

  try {
    const { payload } = await jwtVerify(token, secret);
    return payload.userId as string;
  } catch {
    return null;
  }
}

تمرين: بناء نموذج اتصال آمن

أنشئ نموذج اتصال يحتوي على:

  1. حماية CSRF برموز
  2. التحقق من صحة المدخلات باستخدام Zod
  3. منع XSS بالتعقيم
  4. تحديد المعدل (5 طلبات لكل 10 دقائق لكل IP)
  5. رؤوس CSP مع nonce للنصوص المضمنة
  6. معالجة أخطاء مناسبة دون كشف معلومات حساسة
  7. إشعار بالبريد الإلكتروني بمحتوى معقم

مكافأة: أضف reCAPTCHA v3 لحماية الروبوتات ونفذ حقول honeypot.

أمان متغيرات البيئة

الاستخدام الآمن لمتغيرات البيئة

// ❌ خطير: كشف الأسرار للعميل
// next.config.js
module.exports = {
  env: {
    DATABASE_URL: process.env.DATABASE_URL // مكشوف للعميل!
  }
};

// ✅ آمن: اكشف المتغيرات العامة فقط
module.exports = {
  env: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL
  }
};

// ✅ آمن: استخدم متغيرات البيئة للخادم فقط
// يمكن لـ Server Components الوصول إلى جميع متغيرات env مباشرة
export default async function Page() {
  const data = await fetch(process.env.INTERNAL_API_URL);
  return <div>{/* render */}</div>;
}

قائمة التحقق من الأمان

  • ✅ فرض HTTPS في الإنتاج
  • ✅ حماية CSRF على الطلبات المغيرة للحالة
  • ✅ التحقق من صحة المدخلات والتعقيم
  • ✅ تكوين رؤوس CSP
  • ✅ تحديد المعدل على نقاط النهاية الحساسة
  • ✅ إدارة جلسة آمنة
  • ✅ تجزئة كلمة المرور باستخدام bcrypt/argon2
  • ✅ منع حقن SQL باستخدام ORMs
  • ✅ منع XSS بالهروب المناسب
  • ✅ تكوين رؤوس الأمان
  • ✅ تأمين متغيرات البيئة
  • ✅ تحديث التبعيات بانتظام
  • ✅ عمليات تدقيق الأمان باستخدام npm audit

نصيحة محترف: استخدم أدوات مثل npm audit و Snyk و OWASP ZAP لفحص الثغرات بانتظام في تطبيقك وتبعياته.