Cours Next.js · Module 7: Authentification et autorisation

Sécurité de l’application et OWASP

20 min de lecture
Dans cette leçon5

In previous modules, we focused on implementing authentication, session management, the role and permission system, route protection middleware, and the Context Provider for authentication. Now we will shift our attention to the broader area of web application security, with particular focus on OWASP (Open Web Application Security Project) guidelines.

OWASP is a non-profit organization that works to improve software security. It regularly publishes the Top 10 list of the most serious security threats for web applications, which is a fundamental reference point for developers striving to create secure applications.

OWASP Top 10 et leur application dans les applications Next.js

We will look at how to secure Next.js applications against the most serious threats from the OWASP Top 10 list:

1. Broken Access Control

Threat: Improper implementation of access control, allowing unauthorized users to access protected resources or perform unauthorized operations.

Zabezpieczenia w Next.js:

1// 1. Middleware do ochrony tras
2// middleware.ts
3import { NextResponse } from 'next/server';
4import type { NextRequest } from 'next/server';
5import { getToken } from 'next-auth/jwt';
6
7export async function middleware(request: NextRequest) {
8  const token = await getToken({ req: request });
9
10  // Check user permissions
11  if (!token) {
12    return NextResponse.redirect(new URL('/auth/login', request.url));
13  }
14
15  // Role-based access control
16  if (request.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
17    return NextResponse.redirect(new URL('/unauthorized', request.url));
18  }
19
20  return NextResponse.next();
21}
22
23// 2. Weryfikacja permissions w Route Handlers
24// app/api/protected/resource/route.ts
25import { NextResponse } from 'next/server';
26import { getServerSession } from 'next-auth/next';
27import { authOptions } from '@/app/api/auth/[...nextauth]/route';
28
29export async function GET() {
30  const session = await getServerSession(authOptions);
31
32  if (!session) {
33    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
34  }
35
36  // Vérification des permissions pour une ressource spécifique
37  const hasPermission = await checkResourcePermission(session.user.id, 'resource-id');
38
39  if (!hasPermission) {
40    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
41  }
42
43  // Continue with resource access...
44}
45
46// Function that checks permissions to a resource
47async function checkResourcePermission(userId: string, resourceId: string) {
48  // Implémentation de la vérification des permissions, p.ex. via une requête à la base de données
49  // It's worth implementing a caching mechanism for frequently checked permissions
50  return true; // Sample implementation
51}

Dodatkowe najlepsze praktyki:

  1. Implement access control at the server level, never rely solely on client-side security
  2. Apply the default deny principle (deny by default)
  3. Regularly review and test access control mechanisms
  4. Implement an account lockout mechanism after multiple failed login attempts

2. Cryptographic Failures

Threat: Improper or insufficient application of cryptography, leading to the disclosure of sensitive data.

Zabezpieczenia w Next.js:

1// 1. Secure password storage
2// lib/auth.ts
3import { hash, compare } from 'bcrypt';
4
5// Hashing password during registration
6export async function hashPassword(password: string): Promise<string> {
7  // We use 12 rounds for a good compromise between security and performance
8  return hash(password, 12);
9}
10
11// Verifying password during login
12export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
13  return compare(password, hashedPassword);
14}
15
16// 2. Encrypting sensitive data in the database
17// lib/encryption.ts
18import crypto from 'crypto';
19
20const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY as string; // Must be 32 bytes (256 bits)
21const IV_LENGTH = 16; // For AES, initialization vector is 16 bytes
22
23export function encrypt(text: string): string {
24  const iv = crypto.randomBytes(IV_LENGTH);
25  const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
26  let encrypted = cipher.update(text, 'utf8', 'hex');
27  encrypted += cipher.final('hex');
28  return `${iv.toString('hex')}:${encrypted}`;
29}
30
31export function decrypt(text: string): string {
32  const [ivHex, encryptedHex] = text.split(':');
33  const iv = Buffer.from(ivHex, 'hex');
34  const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
35  let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
36  decrypted += decipher.final('utf8');
37  return decrypted;
38}

Dodatkowe najlepsze praktyki:

  1. Always use HTTPS for all application pages
  2. Implement appropriate security-related HTTP headers (HSTS, CSP)
  3. Mets régulièrement à jour les bibliothèques cryptographiques
  4. Implement a cryptographic key rotation strategy
  5. Do not store sensitive data in local browser storages (localStorage, sessionStorage)

3. Injection (Wstrzykiwanie)

Threat: Injection of untrusted data into interpreters as part of a command or query, which can lead to execution of unauthorized commands or data access.

Zabezpieczenia w Next.js:

1// 1. Requêtes sécurisées vers la base de données (utilisation d’un ORM)
2// app/api/users/route.ts
3import { NextResponse } from 'next/server';
4import { prisma } from '@/lib/prisma';
5
6export async function GET(request: Request) {
7  const { searchParams } = new URL(request.url);
8  const name = searchParams.get('name');
9
10  // Requête sécurisée avec un ORM (Prisma)
11  // Instead of direct injection of parameters into an SQL query
12  const users = await prisma.user.findMany({
13    where: {
14      name: {
15        contains: name || '',
16      },
17    },
18    select: {
19      id: true,
20      name: true,
21      email: true,
22    },
23  });
24
25  return NextResponse.json(users);
26}
27
28// 2. Input data validation using the zod library
29// app/api/products/route.ts
30import { NextResponse } from 'next/server';
31import { z } from 'zod';
32
33// Validation schema
34const ProductSchema = z.object({
35  name: z.string().min(1).max(100),
36  price: z.number().positive(),
37  description: z.string().optional(),
38  categoryId: z.string().uuid(),
39});
40
41export async function POST(request: Request) {
42  try {
43    const body = await request.json();
44
45    // Validate input data
46    const result = ProductSchema.safeParse(body);
47
48    if (!result.success) {
49      return NextResponse.json(
50        { error: 'Invalid input', details: result.error.format() },
51        { status: 400 }
52      );
53    }
54
55    // Traitement ultérieur des données sécurisées et validées...
56
57  } catch (error) {
58    return NextResponse.json(
59      { error: 'Server error' },
60      { status: 500 }
61    );
62  }
63}

Dodatkowe najlepsze praktyki:

  1. Filter and sanitize input and output data
  2. Use complex, randomized passwords and session identifiers
  3. Implement session timeout and limit login attempts
  4. Échappe toutes les données avant de les placer dans des requêtes SQL, des scripts ou du HTML
  5. Avoid dynamic generation of JavaScript code by string concatenation

4. Insecure Design (Niebezpieczny projekt)

Threat: Lack of appropriate security mechanisms at the application design level, leading to fundamental security vulnerabilities.

Zabezpieczenia w Next.js:

1// 1. Password security policy implementation
2// lib/passwordPolicy.ts
3export function isPasswordStrong(password: string): { isValid: boolean; reason?: string } {
4  // Minimum length of 8 characters
5  if (password.length < 8) {
6    return { isValid: false, reason: 'Password must be at least 8 characters' };
7  }
8
9  // Wymaga wielkich liter
10  if (!/[A-Z]/.test(password)) {
11    return { isValid: false, reason: 'Password must contain at least one uppercase letter' };
12  }
13
14  // Requires lowercase letters
15  if (!/[a-z]/.test(password)) {
16    return { isValid: false, reason: 'Password must contain at least one lowercase letter' };
17  }
18
19  // Wymaga cyfr
20  if (!/[0-9]/.test(password)) {
21    return { isValid: false, reason: 'Password must contain at least one digit' };
22  }
23
24  // Requires special characters
25  if (!/[^A-Za-z0-9]/.test(password)) {
26    return { isValid: false, reason: 'Password must contain at least one special character' };
27  }
28
29  // Check if the password is not on the list of common, easily guessable passwords
30  const commonPasswords = ['Password123!', 'Admin123!', '12345678Aa!', 'Qwerty123!']; // In practice, this should be a larger database
31  if (commonPasswords.includes(password)) {
32    return { isValid: false, reason: 'Password is too common and easy to guess' };
33  }
34
35  return { isValid: true };
36}
37
38// 2. Implémentation d’un système de permissions basé sur le modèle de données
39// lib/permissions.ts
40import { prisma } from '@/lib/prisma';
41
42// Model allowing certain operations only to the resource owner
43export async function canModifyResource(userId: string, resourceId: string): Promise<boolean> {
44  const resource = await prisma.resource.findUnique({
45    where: { id: resourceId },
46    select: { userId: true },
47  });
48
49  // Resource does not exist or the user is not the owner
50  if (!resource || resource.userId !== userId) {
51    return false;
52  }
53
54  return true;
55}

Dodatkowe najlepsze praktyki:

  1. Implementuj mechanizm wielopoziomowej ochrony (defense in depth)
  2. Perform threat modeling at an early stage of application development
  3. Design with the principle of least privilege in mind
  4. Traite les données utilisateur comme non fiables et valide-les toujours
  5. Regularly review and update security mechanisms

5. Security Misconfiguration

Threat: Improper security configuration in any application component, which can lead to unauthorized access or data leakage.

Zabezpieczenia w Next.js:

1// 1. Configuration de la Content Security Policy
2// next.config.js
3const ContentSecurityPolicy = `
4  default-src 'self';
5  script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.example.com;
6  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
7  font-src 'self' https://fonts.gstatic.com;
8  img-src 'self' data: https:;
9  connect-src 'self' https://api.example.com;
10  frame-src 'self';
11`;
12
13module.exports = {
14  async headers() {
15    return [
16      {
17        source: '/(.*)',
18        headers: [
19          {
20            key: 'Content-Security-Policy',
21            value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
22          },
23          {
24            key: 'X-Content-Type-Options',
25            value: 'nosniff',
26          },
27          {
28            key: 'X-Frame-Options',
29            value: 'DENY',
30          },
31          {
32            key: 'X-XSS-Protection',
33            value: '1; mode=block',
34          },
35          {
36            key: 'Referrer-Policy',
37            value: 'strict-origin-when-cross-origin',
38          },
39          {
40            key: 'Permissions-Policy',
41            value: 'camera=(), microphone=(), geolocation=(self)',
42          },
43        ],
44      },
45    ];
46  },
47};
48
49// 2. Secure use of environment variables
50// lib/config.ts
51interface Config {
52  database: {
53    url: string;
54  };
55  auth: {
56    secret: string;
57    tokenExpiration: number;
58  };
59  services: {
60    apiKey: string;
61    endpoint: string;
62  };
63}
64
65export const config: Config = {
66  database: {
67    url: process.env.DATABASE_URL || '',
68  },
69  auth: {
70    secret: process.env.AUTH_SECRET || '',
71    tokenExpiration: Number(process.env.TOKEN_EXPIRATION) || 3600,
72  },
73  services: {
74    apiKey: process.env.SERVICE_API_KEY || '',
75    endpoint: process.env.SERVICE_ENDPOINT || '',
76  },
77};
78
79// Fonction pour vérifier la configuration au démarrage de l'application
80export function validateConfig() {
81  const requiredEnvVars = [
82    'DATABASE_URL',
83    'AUTH_SECRET',
84    'SERVICE_API_KEY',
85    'SERVICE_ENDPOINT',
86  ];
87
88  const missingEnvVars = requiredEnvVars.filter(
89    (envVar) => !process.env[envVar]
90  );
91
92  if (missingEnvVars.length > 0) {
93    throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
94  }
95
96  if (process.env.AUTH_SECRET && process.env.AUTH_SECRET.length < 32) {
97    throw new Error('AUTH_SECRET should be at least 32 characters long for security');
98  }
99
100  console.log('Configuration validated successfully');
101}

Dodatkowe najlepsze praktyki:

  1. Remove or disable unnecessary features, components, and dependencies
  2. Regularly update all application components and dependencies
  3. Implementuj mechanizm audytowania konfiguracji
  4. Never place sensitive data in source code or publicly accessible files
  5. Use different configurations for development and production environments

6. Vulnerable and Outdated Components

Threat: Using components and libraries containing known security vulnerabilities or that are no longer supported.

Zabezpieczenia w Next.js:

1// 1. Automatic dependency scanning
2// package.json
3{
4  "scripts": {
5    "audit": "npm audit",
6    "audit:fix": "npm audit fix",
7    "outdated": "npm outdated",
8    "update": "npm update",
9    "deps:check": "npx depcheck",
10    "prepare": "husky install"
11  },
12  "husky": {
13    "hooks": {
14      "pre-commit": "npm run audit"
15    }
16  }
17}
18
19// 2. Introduction de la politique SRI (Subresource Integrity)
20// For critical external JavaScript libraries
21// app/layout.tsx (example)
22import Script from 'next/script';
23
24export default function RootLayout({ children }: { children: React.ReactNode }) {
25  return (
26    <html lang="en">
27      <head>
28        {/* Using SRI to verify the integrity of an external script */}
29        <Script
30          src="https://cdn.example.com/library.min.js"
31          integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
32          crossOrigin="anonymous"
33        />
34      </head>
35      <body>{children}</body>
36    </html>
37  );
38}

Dodatkowe najlepsze praktyki:

  1. Regularly running npm audit or using tools like Snyk, Dependabot
  2. Removing unused dependencies
  3. Subscribing to notifications about security vulnerabilities in used libraries
  4. Using trusted, well-maintained libraries with an active community
  5. Preparing procedures for quick component updates after vulnerability discovery

7. Identification and Authentication Failures

Threat: Improper implementation of authentication mechanisms, allowing attackers to access user accounts.

Zabezpieczenia w Next.js:

1// 1. Login attempt limiting implementation
2// lib/rateLimit.ts
3import { Redis } from '@upstash/redis';
4
5const redis = new Redis({
6  url: process.env.UPSTASH_REDIS_URL!,
7  token: process.env.UPSTASH_REDIS_TOKEN!,
8});
9
10export async function rateLimit(
11  identifier: string, // IP lub user identifier
12  limit: number, // Maximum number of attempts
13  window: number // Okno czasowe w sekundach
14): Promise<{ success: boolean; remaining: number; reset: number }> {
15  const now = Math.floor(Date.now() / 1000);
16  const key = `ratelimit:${identifier}`;
17
18  // Récupérer les données actuelles sur les limitations
19  const entry = await redis.hgetall(key);
20
21  if (!entry.count) {
22    // Pierwszy wpis
23    await redis.hset(key, {
24      count: 1,
25      reset: now + window,
26    });
27    await redis.expire(key, window);
28
29    return {
30      success: true,
31      remaining: limit - 1,
32      reset: now + window,
33    };
34  }
35
36  // Check if the time window has passed
37  if (parseInt(entry.reset) < now) {
38    // Resetuj licznik
39    await redis.hset(key, {
40      count: 1,
41      reset: now + window,
42    });
43    await redis.expire(key, window);
44
45    return {
46      success: true,
47      remaining: limit - 1,
48      reset: now + window,
49    };
50  }
51
52  // Check if the limit has been exceeded
53  const count = parseInt(entry.count);
54  if (count >= limit) {
55    return {
56      success: false,
57      remaining: 0,
58      reset: parseInt(entry.reset),
59    };
60  }
61
62  // Mettre à jour le compteur
63  await redis.hincrby(key, 'count', 1);
64
65  return {
66    success: true,
67    remaining: limit - count - 1,
68    reset: parseInt(entry.reset),
69  };
70}
71
72// 2. Login page implementation with attempt limiting
73// app/api/auth/login/route.ts
74import { NextResponse } from 'next/server';
75import { rateLimit } from '@/lib/rateLimit';
76
77export async function POST(request: Request) {
78  try {
79    const { email, password } = await request.json();
80
81    // Récupérer l’adresse IP de l’utilisateur
82    const ip = request.headers.get('x-forwarded-for') || 'unknown';
83
84    // Limit login attempts: 5 attempts within 15 minutes
85    const rateLimitResult = await rateLimit(ip, 5, 15 * 60);
86
87    if (!rateLimitResult.success) {
88      return NextResponse.json(
89        {
90          error: 'Too many login attempts. Please try again later.',
91          reset: rateLimitResult.reset,
92        },
93        { status: 429 }
94      );
95    }
96
97    // Kontynuuj proces logowania...
98
99  } catch (error) {
100    console.error('Login error:', error);
101    return NextResponse.json(
102      { error: 'An error occurred during login' },
103      { status: 500 }
104    );
105  }
106}

Dodatkowe najlepsze praktyki:

  1. Implement strong password requirements
  2. Require multi-factor authentication (MFA) for administrator accounts and users with high permissions
  3. Do not reveal detailed information about authentication errors
  4. Implement secure password recovery mechanisms
  5. Use one-time token mechanisms for critical operations

8. Software and Data Integrity Failures

Threat: Lack of software and data integrity verification, allowing the introduction of incorrect or malicious components.

Zabezpieczenia w Next.js:

1// 1. Implementation of a digital signature mechanism for critical data
2// lib/signatures.ts
3import crypto from 'crypto';
4
5const SIGNATURE_KEY = process.env.SIGNATURE_KEY!;
6
7// Génération de la signature pour les données
8export function signData(data: any): string {
9  const dataString = typeof data === 'string' ? data : JSON.stringify(data);
10  return crypto
11    .createHmac('sha256', SIGNATURE_KEY)
12    .update(dataString)
13    .digest('hex');
14}
15
16// Vérification de la signature des données
17export function verifySignature(data: any, signature: string): boolean {
18  const expectedSignature = signData(data);
19  return crypto.timingSafeEqual(
20    Buffer.from(expectedSignature, 'hex'),
21    Buffer.from(signature, 'hex')
22  );
23}
24
25// 2. Implementation of webhook verification for external services
26// app/api/webhooks/payment-processor/route.ts
27import { NextResponse } from 'next/server';
28import { verifySignature } from '@/lib/signatures';
29
30export async function POST(request: Request) {
31  const body = await request.text();
32  const signature = request.headers.get('X-Webhook-Signature') || '';
33
34  // Weryfikacja podpisu webhook
35  if (!verifySignature(body, signature)) {
36    console.error('Invalid webhook signature');
37    return NextResponse.json(
38      { error: 'Invalid signature' },
39      { status: 401 }
40    );
41  }
42
43  // Traitement du webhook vérifié...
44  const data = JSON.parse(body);
45
46  //
47  return NextResponse.json({ status: 'success' });
48}

Dodatkowe najlepsze praktyki:

  1. Verify the integrity of files and source code in the CI/CD process
  2. Verify the origin of external libraries and components
  3. Use SRI (Subresource Integrity) mechanisms for external resources
  4. Implement automated security tests in the CI/CD process
  5. Surveille et analyse les changements dans les fichiers de configuration et le code de l'application

9. Security Logging and Monitoring Failures

Threat: Insufficient logging, monitoring, and alerting of security events, which prevents detection, escalation, and response to incidents.

Zabezpieczenia w Next.js:

1// 1. Implementacja rozbudowanego logowania
2// lib/logger.ts
3import winston from 'winston';
4
5const logger = winston.createLogger({
6  level: process.env.LOG_LEVEL || 'info',
7  format: winston.format.combine(
8    winston.format.timestamp(),
9    winston.format.json()
10  ),
11  defaultMeta: { service: 'next-app' },
12  transports: [
13    // Local log saving at error level
14    new winston.transports.File({ filename: 'error.log', level: 'error' }),
15    // Local saving of all logs
16    new winston.transports.File({ filename: 'combined.log' }),
17  ],
18});
19
20// In production, send logs to a monitoring service
21if (process.env.NODE_ENV === 'production') {
22  // Example for Datadog
23  // Wymaga instalacji '@datadog/winston'
24  // import { datadog } from '@datadog/winston';
25  // logger.add(new datadog({
26  //   apiKey: process.env.DATADOG_API_KEY,
27  //   hostname: process.env.HOSTNAME,
28  //   service: 'next-app',
29  //   ddsource: 'nodejs',
30  // }));
31}
32
33// Functions for logging security events
34export function logSecurityEvent(
35  eventType: string,
36  data: any,
37  severity: 'info' | 'warn' | 'error' = 'info'
38) {
39  logger.log({
40    level: severity,
41    message: `Security event: ${eventType}`,
42    eventType,
43    data,
44    timestamp: new Date().toISOString(),
45  });
46}
47
48export function logAuthEvent(
49  userId: string | null,
50  action: 'login' | 'logout' | 'failed_login' | 'password_reset' | 'account_locked',
51  details: any = {}
52) {
53  logSecurityEvent(
54    `auth_${action}`,
55    {
56      userId,
57      ip: details.ip,
58      userAgent: details.userAgent,
59      ...details,
60    },
61    action === 'failed_login' || action === 'account_locked' ? 'warn' : 'info'
62  );
63}
64
65// 2. Implementacja middleware do logowania requests HTTP
66// middleware.ts
67import { NextResponse } from 'next/server';
68import type { NextRequest } from 'next/server';
69import { logSecurityEvent } from '@/lib/logger';
70
71export function middleware(request: NextRequest) {
72  // Save basic request information
73  const requestInfo = {
74    method: request.method,
75    path: request.nextUrl.pathname,
76    ip: request.ip || 'unknown',
77    userAgent: request.headers.get('user-agent') || 'unknown',
78    referer: request.headers.get('referer') || 'unknown',
79  };
80
81  // Loguj tylko istotne paths (pomijaj zasoby statyczne, itp.)
82  if (!request.nextUrl.pathname.match(/\.(ico|png|jpg|jpeg|svg|css|js)$/)) {
83    logSecurityEvent('http_request', requestInfo);
84  }
85
86  // Potential attack detection
87  if (detectPotentialAttack(request)) {
88    logSecurityEvent('potential_attack', requestInfo, 'warn');
89  }
90
91  return NextResponse.next();
92}
93
94// Function that detects potential attacks based on request patterns
95function detectPotentialAttack(request: NextRequest): boolean {
96  const url = request.nextUrl.toString().toLowerCase();
97  const body = request.body; // May be unavailable in middleware
98
99  // Detection of basic attack attempts
100  const attackPatterns = [
101    /(\.\.\/|\.\.\\/)/i, // Path traversal
102    /('|")(;|--|\+|\%|\#|\)/i, // Podstawowe SQL injection
103    /<script>[^<]*<\/script>/i, // Basic XSS
104    /etc\/passwd/i, // Path disclosure
105    /\/(wp-admin|wp-content|wp-includes)/i, // WordPress scanning
106  ];
107
108  return attackPatterns.some(pattern => pattern.test(url));
109}

Dodatkowe najlepsze praktyki:

  1. Implementuj centralizowany system logowania i monitorowania
  2. Konfiguruj automatyczne alertowanie o podejrzanych zdarzeniach
  3. Provide mechanisms for correlating events from different parts of the application
  4. Regularly analyze logs for unauthorized activities
  5. Store logs for an appropriately long time in accordance with regulations and business needs

10. Server-Side Request Forgery

Threat: SSRF attacks allow an attacker to invoke HTTP requests from the application server to internal or external resources to which the server has access.

Zabezpieczenia w Next.js:

1// 1. Secure URL fetching implementation
2// lib/safeFetch.ts
3import { URL } from 'url';
4import fetch from 'node-fetch';
5
6// Lista dozwolonych domen
7const ALLOWED_HOSTS = [
8  'api.example.com',
9  'api.trusted-service.com',
10  'cdn.example.com',
11];
12
13// List of forbidden IP addresses (private IP ranges)
14const BLOCKED_IP_RANGES = [
15  // Lokalne
16  /^127\.\d+\.\d+\.\d+$/, // 127.0.0.0/8
17  /^10\.\d+\.\d+\.\d+$/, // 10.0.0.0/8
18  /^172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+$/, // 172.16.0.0/12
19  /^192\.168\.\d+\.\d+$/, // 192.168.0.0/16
20  // Lokalne linki
21  /^169\.254\.\d+\.\d+$/, // 169.254.0.0/16
22  // Multicast
23  /^2(2[4-9]|3\d)\.\d+\.\d+\.\d+$/, // 224.0.0.0/4
24];
25
26export async function safeFetch(urlString: string, options: RequestInit = {}) {
27  try {
28    // Parse URL to check its components
29    const url = new URL(urlString);
30
31    // Check if the domain is on the allowed list
32    if (!ALLOWED_HOSTS.includes(url.hostname)) {
33      throw new Error(`Host not allowed: ${url.hostname}`);
34    }
35
36    // Check if the IP address is not on the blocked list
37    // To wymaga dodatkowej logiki do rozpoznawania DNS
38    // Simplified version of checking directly by host
39    if (BLOCKED_IP_RANGES.some(pattern => pattern.test(url.hostname))) {
40      throw new Error(`IP address not allowed: ${url.hostname}`);
41    }
42
43    // Check if the protocol is secure
44    if (url.protocol !== 'https:') {
45      throw new Error(`Protocol not allowed: ${url.protocol}`);
46    }
47
48    // Wykonaj request HTTP po weryfikacji URL
49    return fetch(url.toString(), options);
50  } catch (error) {
51    console.error('Safe fetch error:', error);
52    throw new Error(`Invalid or disallowed URL: ${urlString}`);
53  }
54}
55
56// 2. API proxy implementation with URL validation
57// app/api/proxy/route.ts
58import { NextResponse } from 'next/server';
59import { safeFetch } from '@/lib/safeFetch';
60
61export async function GET(request: Request) {
62  try {
63    const { searchParams } = new URL(request.url);
64    const targetUrl = searchParams.get('url');
65
66    if (!targetUrl) {
67      return NextResponse.json(
68        { error: 'URL parameter is required' },
69        { status: 400 }
70      );
71    }
72
73    try {
74      // Use the secure fetch function
75      const response = await safeFetch(targetUrl);
76      const data = await response.text();
77
78      return new NextResponse(data, {
79        status: response.status,
80        headers: {
81          'Content-Type': response.headers.get('Content-Type') || 'text/plain',
82        },
83      });
84    } catch (error) {
85      return NextResponse.json(
86        { error: 'Invalid or disallowed URL' },
87        { status: 400 }
88      );
89    }
90  } catch (error) {
91    console.error('Proxy error:', error);
92    return NextResponse.json(
93      { error: 'Server error' },
94      { status: 500 }
95    );
96  }
97}

Dodatkowe najlepsze praktyki:

  1. Implementuj listy dozwolonych domen zamiast list blokowanych
  2. Use a network firewall to block connections to internal resources
  3. Filter and sanitize input data containing URLs
  4. Use minimal permissions for service and application accounts
  5. Avoid passing full responses from external APIs to the client

Implementacja Security Headers w Next.js

Security headers are a key element in protecting web applications. Next.js allows easy configuration of these headers in the file next.config.js:

1// next.config.js
2const securityHeaders = [
3  {
4    key: 'X-DNS-Prefetch-Control',
5    value: 'on',
6  },
7  {
8    key: 'Strict-Transport-Security',
9    value: 'max-age=63072000; includeSubDomains; preload',
10  },
11  {
12    key: 'X-XSS-Protection',
13    value: '1; mode=block',
14  },
15  {
16    key: 'X-Frame-Options',
17    value: 'SAMEORIGIN',
18  },
19  {
20    key: 'X-Content-Type-Options',
21    value: 'nosniff',
22  },
23  {
24    key: 'Referrer-Policy',
25    value: 'strict-origin-when-cross-origin',
26  },
27  {
28    key: 'Permissions-Policy',
29    value: 'camera=(), microphone=(), geolocation=(self), interest-cohort=()',
30  },
31];
32
33module.exports = {
34  async headers() {
35    return [
36      {
37        // Apply these headers to all paths
38        source: '/(.*)',
39        headers: securityHeaders,
40      },
41    ];
42  },
43  // Inne konfiguracje Next.js...
44};

Penetration testing and security testing automation

Tests are a key element in ensuring application security. Below is an example of integrating security tests with the CI/CD process:

1# .github/workflows/security.yml
2name: Security Checks
3
4on:
5  push:
6    branches: [ main, develop ]
7  pull_request:
8    branches: [ main, develop ]
9  schedule:
10    - cron: '0 0 * * 0' # Cotygodniowe skanowanie
11
12jobs:
13  security-checks:
14    runs-on: ubuntu-latest
15    steps:
16      - uses: actions/checkout@v2
17
18      - name: Setup Node.js
19        uses: actions/setup-node@v2
20        with:
21          node-version: '16'
22
23      - name: Install dependencies
24        run: npm ci
25
26      - name: Run npm audit
27        run: npm audit --audit-level=high
28
29      - name: OWASP ZAP Scan
30        uses: zaproxy/action-baseline@v0.6.1
31        with:
32          target: 'https://staging.example.com' # Your staging environment URL
33
34      - name: Run Snyk to check for vulnerabilities
35        uses: snyk/actions/node@master
36        env:
37          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
38
39      - name: Check for secrets in code
40        uses: gitleaks/gitleaks-action@v1.6.0

Security checklist for Next.js applications

Below is a checklist you can use to assess the security of your Next.js application:

General security

  • Appropriate security-related HTTP headers have been implemented
  • HTTPS is used for all environments
  • Input data validation mechanism has been implemented
  • Inactive sessions are automatically logged out
  • Sensitive information is not stored in publicly accessible files
  • Logs do not contain sensitive data

Uwierzytelnianie i authorization

  • Passwords are properly hashed and salted
  • Login attempt limiting mechanism has been implemented
  • Password security policy has been implemented
  • Role and permission system works correctly
  • Sessions are securely managed
  • Multi-factor authentication (MFA) has been implemented for administrator accounts

Traitement des données

  • Data is properly sanitized before rendering
  • Database queries are parameterized (protected against SQL Injection)
  • XSS prevention mechanisms have been implemented
  • API endpoints are protected against unauthorized access
  • CSRF token implementation prevents CSRF attacks

Dependencies and code

  • All dependencies are up to date and free of known vulnerabilities
  • Code is regularly scanned for security vulnerabilities
  • Best coding practices are applied
  • There are no hardcoded secrets and keys in the code

Monitorowanie i reakcja na incydenty

  • Security event logging mechanism has been implemented
  • Security incident response procedures have been defined
  • Regular penetration tests are conducted
  • Monitoring and alerting mechanisms have been implemented

Summary

Web application security, including Next.js applications, requires a comprehensive approach and constant attention. In this module, we covered the most important threats defined in the OWASP Top 10 and how to minimize them in the context of Next.js applications.

Key points to remember:

  1. Comprehensive approach - security should be considered at every stage of application development
  2. Principle of least permissions - grant minimum permissions necessary to perform the task
  3. Validation and sanitization - always validate and sanitize input data
  4. Updates and patches - regularly update dependencies and apply security patches
  5. Monitorowanie i logowanie - implementuj rozbudowane mechanizmy monitorowania i logowania
  6. Testing - regularly perform security tests

Remember that security is a process, not a product. It requires constant improvement and adaptation to new threats.

In the next module, we will look at security auditing and monitoring to better understand how to detect and respond to potential security incidents.

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Demo: OWASP Top 10
4interface Vulnerability {
5  id: string;
6  name: string;
7  owaspId: string;
8  severity: 'critical' | 'high' | 'medium';
9  description: string;
10  example: string;
11  prevention: string[];
12}
13
14const owaspTop10: Vulnerability[] = [
15  { id: '1', name: 'Broken Access Control', owaspId: 'A01:2021', severity: 'critical',
16    description: 'Absence de contrôle d\'accès - les utilisateurs agissent au-delà de leurs permissions.',
17    example: 'GET /api/users/123/data - no identity verification',
18    prevention: ['Refuse l\'accès par défaut', 'Implémente le RBAC', 'Valide côté serveur'] },
19  { id: '2', name: 'Cryptographic Failures', owaspId: 'A02:2021', severity: 'critical',
20    description: 'Chiffrement faible ou absence de chiffrement des données.',
21    example: 'Mots de passe en clair dans la base de données',
22    prevention: ['bcrypt/argon2 pour les mots de passe', 'HTTPS', 'Ne stocke pas de données inutiles'] },
23  { id: '3', name: 'Injection', owaspId: 'A03:2021', severity: 'critical',
24    description: 'SQL, NoSQL, OS command injection.',
25    example: 'SELECT * FROM users WHERE id = userInput (sans paramétrage)',
26    prevention: ['Requêtes paramétrées', 'Validation des données', 'Utilise un ORM'] },
27  { id: '4', name: 'Insecure Design', owaspId: 'A04:2021', severity: 'high',
28    description: 'Absence de patterns de sécurité dans l\'architecture.',
29    example: 'Absence de rate limiting sur la connexion',
30    prevention: ['Modélisation des menaces', 'Patterns de sécurité', 'Security code review'] },
31  { id: '5', name: 'Security Misconfiguration', owaspId: 'A05:2021', severity: 'high',
32    description: 'Paramètres par défaut, ports ouverts, debug en production.',
33    example: 'Erreurs détaillées en production',
34    prevention: ['Hardening', 'Permissions minimales', 'Audits de configuration'] },
35  { id: '6', name: 'Vulnerable Components', owaspId: 'A06:2021', severity: 'medium',
36    description: 'Bibliothèques avec des vulnérabilités connues.',
37    example: 'Ancienne version de lodash avec prototype pollution',
38    prevention: ['npm audit', 'Mets à jour les dépendances', 'Surveille les CVE'] },
39  { id: '7', name: 'Auth Failures', owaspId: 'A07:2021', severity: 'critical',
40    description: 'Mots de passe faibles, absence de MFA, tokens vulnérables.',
41    example: 'Absence de limite de tentatives de connexion',
42    prevention: ['MFA', 'Politiques de mots de passe forts', 'Rate limiting'] },
43  { id: '8', name: 'Data Integrity', owaspId: 'A08:2021', severity: 'high',
44    description: 'Erreurs d\'intégrité logicielle et des données.',
45    example: 'CI/CD sans vérification des artefacts',
46    prevention: ['Signature des artefacts', 'Vérification de l\'intégrité', 'CI/CD sécurisé'] },
47];
48
49const sevColors: Record<string, string> = { critical: '#ef4444', high: '#f59e0b', medium: '#3b82f6' };
50
51function OWASPDemo() {
52  const [selected, setSelected] = useState<Vulnerability | null>(null);
53  const [checked, setChecked] = useState<Set<string>>(new Set());
54  const toggle = (id: string) => setChecked(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
55  const score = Math.round((checked.size / owaspTop10.length) * 100);
56
57  return (
58    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
59      <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>OWASP Top 10 Checklist</h1>
60
61      <div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '24px', padding: '16px', background: '#1a1a2e', borderRadius: '8px', border: '1px solid #333' }}>
62        <div style={{ width: '60px', height: '60px', borderRadius: '50%', border: '3px solid ' + (score >= 75 ? '#10b981' : score >= 50 ? '#f59e0b' : '#ef4444'), display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '20px', fontWeight: 'bold', color: score >= 75 ? '#10b981' : score >= 50 ? '#f59e0b' : '#ef4444' }}>{score}%</div>
63        <div><strong>Security Score</strong><div style={{ fontSize: '13px', color: '#8892b0' }}>{checked.size}/{owaspTop10.length} sécurisés</div></div>
64      </div>
65
66      <div style={{ display: 'grid', gridTemplateColumns: selected ? '1fr 1fr' : '1fr', gap: '20px' }}>
67        <div style={{ display: 'grid', gap: '8px' }}>
68          {owaspTop10.map(v => (
69            <div key={v.id} onClick={() => setSelected(v)} style={{
70              padding: '12px 16px', borderRadius: '8px', cursor: 'pointer',
71              background: selected?.id === v.id ? '#1a2a3e' : '#1a1a2e',
72              border: selected?.id === v.id ? '1px solid #64ffda' : '1px solid #333',
73              display: 'flex', alignItems: 'center', gap: '12px',
74            }}>
75              <input type="checkbox" checked={checked.has(v.id)} onChange={() => toggle(v.id)} onClick={e => e.stopPropagation()} />
76              <span style={{ fontSize: '12px', padding: '2px 8px', borderRadius: '12px', background: sevColors[v.severity] + '30', color: sevColors[v.severity] }}>{v.severity}</span>
77              <strong style={{ fontSize: '14px' }}>{v.owaspId}: {v.name}</strong>
78            </div>
79          ))}
80        </div>
81        {selected && (
82          <div style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333' }}>
83            <h3 style={{ color: '#64ffda', marginTop: 0 }}>{selected.owaspId}: {selected.name}</h3>
84            <p style={{ color: '#8892b0' }}>{selected.description}</p>
85            <div style={{ background: '#0f0f23', padding: '12px', borderRadius: '6px', marginBottom: '12px' }}>
86              <strong style={{ color: '#ef4444', fontSize: '13px' }}>Exemple :</strong>
87              <pre style={{ fontSize: '12px', color: '#f59e0b', margin: '8px 0 0', whiteSpace: 'pre-wrap' }}>{selected.example}</pre>
88            </div>
89            <strong style={{ color: '#10b981', fontSize: '13px' }}>Prévention :</strong>
90            <ul style={{ fontSize: '13px', color: '#8892b0', paddingLeft: '20px', margin: '8px 0 0' }}>
91              {selected.prevention.map((p, i) => <li key={i}>{p}</li>)}
92            </ul>
93          </div>
94        )}
95      </div>
96    </div>
97  );
98}
99
100export default OWASPDemo;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. L'authentification multifacteur (MFA) ajoute :

  2. 2. Les attaques CSRF peuvent être évitées par :

Voici 2 des 6 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Place la syntaxe d'import et d'utilisation de redirect

  • Clique dans l'ordre

    Place la structure du jeton JWT :

  • Tri vertical

    Place les éléments dans le bon ordre : Suivre les requêtes → Vérifier les limites → Bloquer/Auto

  • Tri vertical

    Place le cycle de vie du jeton JWT :

Articles utiles