Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Bezpieczeństwo aplikacji i OWASP

W poprzednich modułach skupialiśmy się na implementacji uwierzytelniania, zarządzaniu sesjami, systemie ról i uprawnień, middleware ochrony tras oraz Context Providerze dla autentykacji. Teraz przeniesiemy naszą uwagę na szerszy obszar bezpieczeństwa aplikacji internetowych, ze szczególnym uwzględnieniem wytycznych OWASP (Open Web Application Security Project).

OWASP to organizacja non-profit, która pracuje nad poprawą bezpieczeństwa oprogramowania. Regularnie publikuje listę Top 10 najpoważniejszych zagrożeń bezpieczeństwa dla aplikacji internetowych, która stanowi podstawowy punkt odniesienia dla developerów dążących do tworzenia bezpiecznych aplikacji.

OWASP Top 10 i ich zastosowanie w aplikacjach Next.js

Przyjrzymy się, jak zabezpieczyć aplikacje Next.js przed najpoważniejszymi zagrożeniami z listy OWASP Top 10:

1. Broken Access Control (Uszkodzona kontrola dostępu)

Zagrożenie: Nieprawidłowa implementacja kontroli dostępu, pozwalająca nieautoryzowanym użytkownikom na dostęp do chronionych zasobów lub wykonywanie niedozwolonych operacji.

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  // Sprawdź uprawnienia użytkownika
11  if (!token) {
12    return NextResponse.redirect(new URL('/auth/login', request.url));
13  }
14  
15  // Kontrola dostępu oparta na rolach
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 uprawnień 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  // Sprawdzanie uprawnień dla konkretnego zasobu
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  // Kontynuuj z dostępem do zasobu...
44}
45
46// Funkcja sprawdzająca uprawnienia do zasobu
47async function checkResourcePermission(userId: string, resourceId: string) {
48  // Implementacja sprawdzania uprawnień, np. poprzez zapytanie do bazy danych
49  // Warto zaimplementować mechanizm cache'owania dla często sprawdzanych uprawnień
50  return true; // Przykładowa implementacja
51}

Dodatkowe najlepsze praktyki:

  1. Implementuj kontrolę dostępu na poziomie serwera, nigdy nie polegaj wyłącznie na zabezpieczeniach po stronie klienta
  2. Stosuj zasadę domyślnej odmowy (deny by default)
  3. Regularnie przeglądaj i testuj mechanizmy kontroli dostępu
  4. Implementuj mechanizm blokowania kont po wielu nieudanych próbach logowania

2. Cryptographic Failures (Błędy kryptograficzne)

Zagrożenie: Nieprawidłowe lub niewystarczające zastosowanie kryptografii, prowadzące do ujawnienia wrażliwych danych.

Zabezpieczenia w Next.js:

1// 1. Bezpieczne przechowywanie haseł
2// lib/auth.ts
3import { hash, compare } from 'bcrypt';
4
5// Haszowanie hasła podczas rejestracji
6export async function hashPassword(password: string): Promise<string> {
7  // Używamy 12 rund dla dobrego kompromisu między bezpieczeństwem a wydajnością
8  return hash(password, 12);
9}
10
11// Weryfikacja hasła podczas logowania
12export async function verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
13  return compare(password, hashedPassword);
14}
15
16// 2. Szyfrowanie wrażliwych danych w bazie danych
17// lib/encryption.ts
18import crypto from 'crypto';
19
20const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY as string; // Musi być 32 bajtów (256 bitów)
21const IV_LENGTH = 16; // Dla AES, initialization vector wynosi 16 bajtów
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. Zawsze używaj HTTPS dla wszystkich stron aplikacji
  2. Wdrażaj odpowiednie nagłówki HTTP związane z bezpieczeństwem (HSTS, CSP)
  3. Regularnie aktualizuj biblioteki kryptograficzne
  4. Wdrażaj strategię rotacji kluczy kryptograficznych
  5. Nie przechowuj wrażliwych danych w lokalnych magazynach przeglądarki (localStorage, sessionStorage)

3. Injection (Wstrzykiwanie)

Zagrożenie: Wstrzykiwanie niezaufanych danych do interpreterów jako części komendy lub zapytania, co może prowadzić do wykonania nieautoryzowanych poleceń lub uzyskania dostępu do danych.

Zabezpieczenia w Next.js:

1// 1. Bezpieczne zapytania do bazy danych (wykorzystanie 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  // Bezpieczne zapytanie z wykorzystaniem ORM (Prisma)
11  // Zamiast bezpośredniego wstrzykiwania parametrów do zapytania SQL
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. Walidacja danych wejściowych z wykorzystaniem biblioteki zod
29// app/api/products/route.ts
30import { NextResponse } from 'next/server';
31import { z } from 'zod';
32
33// Schemat walidacji
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    // Walidacja danych wejściowych
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    // Dalsze przetwarzanie bezpiecznych, zwalidowanych danych...
56    
57  } catch (error) {
58    return NextResponse.json(
59      { error: 'Server error' },
60      { status: 500 }
61    );
62  }
63}

Dodatkowe najlepsze praktyki:

  1. Filtruj i sanityzuj dane wejściowe i wyjściowe
  2. Stosuj złożone, randomizowane hasła i identyfikatory sesji
  3. Implementuj timeout sesji i ograniczaj liczbę prób logowania
  4. Escape wszelkie dane przed umieszczeniem ich w poleceniach SQL, skryptach, czy HTML
  5. Unikaj dynamicznego generowania kodu JavaScript przez łączenie ciągów

4. Insecure Design (Niebezpieczny projekt)

Zagrożenie: Brak odpowiednich mechanizmów bezpieczeństwa na poziomie projektowania aplikacji, co prowadzi do fundamentalnych luk w zabezpieczeniach.

Zabezpieczenia w Next.js:

1// 1. Implementacja polityki bezpieczeństwa haseł
2// lib/passwordPolicy.ts
3export function isPasswordStrong(password: string): { isValid: boolean; reason?: string } {
4  // Minimalna długość 8 znaków
5  if (password.length < 8) {
6    return { isValid: false, reason: 'Hasło musi mieć co najmniej 8 znaków' };
7  }
8  
9  // Wymaga wielkich liter
10  if (!/[A-Z]/.test(password)) {
11    return { isValid: false, reason: 'Hasło musi zawierać co najmniej jedną wielką literę' };
12  }
13  
14  // Wymaga małych liter
15  if (!/[a-z]/.test(password)) {
16    return { isValid: false, reason: 'Hasło musi zawierać co najmniej jedną małą literę' };
17  }
18  
19  // Wymaga cyfr
20  if (!/[0-9]/.test(password)) {
21    return { isValid: false, reason: 'Hasło musi zawierać co najmniej jedną cyfrę' };
22  }
23  
24  // Wymaga znaków specjalnych
25  if (!/[^A-Za-z0-9]/.test(password)) {
26    return { isValid: false, reason: 'Hasło musi zawierać co najmniej jeden znak specjalny' };
27  }
28  
29  // Sprawdzenie, czy hasło nie jest na liście popularnych, łatwych do odgadnięcia haseł
30  const commonPasswords = ['Password123!', 'Admin123!', '12345678Aa!', 'Qwerty123!']; // W praktyce powinna być to większa baza
31  if (commonPasswords.includes(password)) {
32    return { isValid: false, reason: 'Hasło jest zbyt popularne i łatwe do odgadnięcia' };
33  }
34  
35  return { isValid: true };
36}
37
38// 2. Implementacja systemu uprawnien w oparciu o model danych
39// lib/permissions.ts
40import { prisma } from '@/lib/prisma';
41
42// Model zezwalaj na określone operacje tylko właścicielowi zasobu
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  // Zasób nie istnieje lub użytkownik nie jest właścicielem
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. Przeprowadzaj modelowanie zagrożeń na wczesnym etapie tworzenia aplikacji
  3. Projektuj z myślą o zasadzie najmniejszych uprawnień (principle of least privilege)
  4. Traktuj dane użytkownika jako niezaufane i zawsze je waliduj
  5. Regularnie przeglądaj i aktualizuj mechanizmy bezpieczeństwa

5. Security Misconfiguration (Błędna konfiguracja bezpieczeństwa)

Zagrożenie: Nieprawidłowa konfiguracja zabezpieczeń w jakimkolwiek komponencie aplikacji, co może prowadzić do nieautoryzowanego dostępu lub wycieku danych.

Zabezpieczenia w Next.js:

1// 1. Konfiguracja 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. Bezpieczne korzystanie ze zmiennych środowiskowych
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// Funkcja do weryfikacji konfiguracji przy starcie aplikacji
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. Usuwaj lub wyłączaj niepotrzebne funkcje, komponenty i zależności
  2. Regularnie aktualizuj wszystkie komponenty aplikacji i zależności
  3. Implementuj mechanizm audytowania konfiguracji
  4. Nigdy nie umieszczaj wrażliwych danych w kodzie źródłowym lub plikach dostępnych publicznie
  5. Używaj różnych konfiguracji dla środowisk deweloperskich i produkcyjnych

6. Vulnerable and Outdated Components (Podatne i przestarzałe komponenty)

Zagrożenie: Używanie komponentów i bibliotek zawierających znane luki bezpieczeństwa lub które nie są już wspierane.

Zabezpieczenia w Next.js:

1// 1. Automatyczne skanowanie zależności
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. Wprowadzenie polityki SRI (Subresource Integrity)
20// Dla krytycznych zewnętrznych bibliotek JavaScript
21// app/layout.tsx (przykład)
22import Script from 'next/script';
23
24export default function RootLayout({ children }: { children: React.ReactNode }) {
25  return (
26    <html lang="en">
27      <head>
28        {/* Wykorzystanie SRI do weryfikacji integralności zewnętrznego skryptu */}
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. Regularne wykonywanie
    npm audit
    lub używanie narzędzi jak Snyk, Dependabot
  2. Usuwanie nieużywanych zależności
  3. Subscrypcja na powiadomienia o lukach bezpieczeństwa w używanych bibliotekach
  4. Stosowanie zaufanych, dobrze utrzymywanych bibliotek z aktywną społecznością
  5. Przygotowanie procedur na szybką aktualizację komponentów po odkryciu luk

7. Identification and Authentication Failures (Błędy identyfikacji i uwierzytelniania)

Zagrożenie: Nieprawidłowa implementacja mechanizmów uwierzytelniania, umożliwiająca atakującym dostęp do kont użytkowników.

Zabezpieczenia w Next.js:

1// 1. Implementacja ograniczania prób logowania
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 identyfikator użytkownika
12  limit: number, // Maksymalna liczba prób
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  // Pobierz aktualne dane o ograniczeniach
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  // Sprawdź, czy okno czasowe minęło
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  // Sprawdź, czy przekroczono limit
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  // Aktualizuj licznik
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. Implementacja strony logowania z ograniczeniem prób
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    // Pobierz adres IP użytkownika
82    const ip = request.headers.get('x-forwarded-for') || 'unknown';
83    
84    // Ogranicz próby logowania: 5 prób w ciągu 15 minut
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. Implementuj silne wymagania dotyczące haseł
  2. Wymagaj uwierzytelniania wieloskładnikowego (MFA) dla kont administratora i użytkowników z wysokimi uprawnieniami
  3. Nie ujawniaj szczegółowych informacji o błędach uwierzytelniania
  4. Implementuj bezpieczne mechanizmy odzyskiwania haseł
  5. Stosuj mechanizm jednorazowych tokenów do operacji krytycznych

8. Software and Data Integrity Failures (Błędy integralności oprogramowania i danych)

Zagrożenie: Brak weryfikacji integralności oprogramowania i danych, umożliwiający wprowadzanie nieprawidłowych lub złośliwych komponentów.

Zabezpieczenia w Next.js:

1// 1. Implementacja mechanizmu podpisów cyfrowych dla krytycznych danych
2// lib/signatures.ts
3import crypto from 'crypto';
4
5const SIGNATURE_KEY = process.env.SIGNATURE_KEY!;
6
7// Generowanie podpisu dla danych
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// Weryfikacja podpisu danych
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. Implementacja weryfikacji webhooków zewnętrznych usług
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  // Przetwarzanie zweryfikowanego webhoka...
44  const data = JSON.parse(body);
45  
46  //
47  return NextResponse.json({ status: 'success' });
48}

Dodatkowe najlepsze praktyki:

  1. Weryfikuj integralność plików i kodów źródłowych w procesie CI/CD
  2. Weryfikuj pochodzenie zewnętrznych bibliotek i komponentów
  3. Używaj mechanizmów SRI (Subresource Integrity) dla zewnętrznych zasobów
  4. Implementuj automatyczne testy bezpieczeństwa w procesie CI/CD
  5. Monitoruj i analizuj zmiany w plikach konfiguracyjnych i kodzie aplikacji

9. Security Logging and Monitoring Failures (Błędy logowania i monitorowania bezpieczeństwa)

Zagrożenie: Niewystarczające logowanie, monitorowanie i alertowanie o zdarzeniach bezpieczeństwa, co uniemożliwia wykrycie, eskalację i odpowiedź na incydenty.

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    // Lokalne zapisywanie logów na poziomie error
14    new winston.transports.File({ filename: 'error.log', level: 'error' }),
15    // Lokalne zapisywanie wszystkich logów
16    new winston.transports.File({ filename: 'combined.log' }),
17  ],
18});
19
20// W środowisku produkcyjnym wysyłaj logi do usługi monitoringu
21if (process.env.NODE_ENV === 'production') {
22  // Przykład dla 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// Funkcje do logowania zdarzeń bezpieczeństwa
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 żądań 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  // Zapisz podstawowe informacje o żądaniu
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 ścieżki (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  // Wykrywanie potencjalnych ataków
87  if (detectPotentialAttack(request)) {
88    logSecurityEvent('potential_attack', requestInfo, 'warn');
89  }
90  
91  return NextResponse.next();
92}
93
94// Funkcja wykrywająca potencjalne ataki na podstawie wzorów w żądaniu
95function detectPotentialAttack(request: NextRequest): boolean {
96  const url = request.nextUrl.toString().toLowerCase();
97  const body = request.body; // Może być niedostępne w middleware
98  
99  // Wykrywanie podstawowych prób ataków
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. Zapewnij mechanizmy korelacji zdarzeń z różnych części aplikacji
  4. Regularnie analizuj logi pod kątem nieautoryzowanych działań
  5. Przechowuj logi przez odpowiednio długi czas zgodnie z regulacjami i potrzebami biznesowymi

10. Server-Side Request Forgery (Fałszerstwo żądań po stronie serwera)

Zagrożenie: Ataki SSRF pozwalają atakującemu na wywoływanie żądań HTTP z serwera aplikacji do zasobów wewnętrznych lub zewnętrznych, do których serwer ma dostęp.

Zabezpieczenia w Next.js:

1// 1. Implementacja bezpiecznego pobierania URLów
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// Lista zabronionych adresów IP (prywatne zakresy IP)
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]|2d|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]|3d).d+.d+.d+$/, // 224.0.0.0/4
24];
25
26export async function safeFetch(urlString: string, options: RequestInit = {}) {
27  try {
28    // Parsuj URL, aby sprawdzić jego komponenty
29    const url = new URL(urlString);
30    
31    // Sprawdź, czy domena jest na liście dozwolonych
32    if (!ALLOWED_HOSTS.includes(url.hostname)) {
33      throw new Error(`Host not allowed: ${url.hostname}`);
34    }
35    
36    // Sprawdź, czy adres IP nie jest na liście zablokowanych
37    // To wymaga dodatkowej logiki do rozpoznawania DNS
38    // Uproszczona wersja sprawdzania bezpośrednio hosta
39    if (BLOCKED_IP_RANGES.some(pattern => pattern.test(url.hostname))) {
40      throw new Error(`IP address not allowed: ${url.hostname}`);
41    }
42    
43    // Sprawdź, czy protokół jest bezpieczny
44    if (url.protocol !== 'https:') {
45      throw new Error(`Protocol not allowed: ${url.protocol}`);
46    }
47    
48    // Wykonaj żądanie 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. Implementacja API proxy z walidacją URL
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      // Użyj bezpiecznej funkcji fetch
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. Używaj zapory sieciowej (firewall) do blokowania połączeń do wewnętrznych zasobów
  3. Filtruj i sanityzuj dane wejściowe zawierające URL
  4. Używaj minimalnych uprawnień dla kont serwisowych i aplikacji
  5. Unikaj przekazywania pełnych odpowiedzi z zewnętrznych API do klienta

Implementacja Security Headers w Next.js

Nagłówki bezpieczeństwa są kluczowym elementem ochrony aplikacji internetowych. Next.js umożliwia łatwą konfigurację tych nagłówków w pliku

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        // Zastosuj te nagłówki do wszystkich ścieżek
38        source: '/(.*)',
39        headers: securityHeaders,
40      },
41    ];
42  },
43  // Inne konfiguracje Next.js...
44};

Testy penetracyjne i automatyzacja testowania bezpieczeństwa

Testy są kluczowym elementem zapewnienia bezpieczeństwa aplikacji. Poniżej przedstawiono przykład integracji testów bezpieczeństwa z procesem CI/CD:

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' # URL Twojego środowiska staging
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

Lista kontrolna bezpieczeństwa dla aplikacji Next.js

Poniżej przedstawiono listę kontrolną, którą możesz wykorzystać do oceny bezpieczeństwa swojej aplikacji Next.js:

Ogólne zabezpieczenia

  • [ ] Wdrożono odpowiednie nagłówki HTTP związane z bezpieczeństwem
  • [ ] Używany jest HTTPS dla wszystkich środowisk
  • [ ] Wdrożono mechanizm walidacji danych wejściowych
  • [ ] Nieaktywne sesje są automatycznie wylogowywane
  • [ ] Wrażliwe informacje nie są przechowywane w plikach dostępnych publicznie
  • [ ] Logi nie zawierają wrażliwych danych

Uwierzytelnianie i autoryzacja

  • [ ] Hasła są odpowiednio haszowane i solone
  • [ ] Zaimplementowano mechanizm ograniczania prób logowania
  • [ ] Wdrożono politykę bezpieczeństwa haseł
  • [ ] System ról i uprawnień działa poprawnie
  • [ ] Sesje są bezpiecznie zarządzane
  • [ ] Zaimplementowano uwierzytelnianie wieloskładnikowe (MFA) dla kont administratora

Przetwarzanie danych

  • [ ] Dane są odpowiednio sanityzowane przed renderowaniem
  • [ ] Zapytania do bazy danych są parametryzowane (zabezpieczone przed SQL Injection)
  • [ ] Wdrożono mechanizmy zapobiegające atakom XSS
  • [ ] API endpoints są zabezpieczone przed nieautoryzowanym dostępem
  • [ ] Implementacja CSRF tokenów zapobiega atakom CSRF

Zależności i kod

  • [ ] Wszystkie zależności są aktualne i wolne od znanych luk
  • [ ] Kod jest regularnie skanowany pod kątem luk bezpieczeństwa
  • [ ] Stosowane są najlepsze praktyki kodowania
  • [ ] Nie ma twardych kodów sekretów i kluczy w kodzie

Monitorowanie i reakcja na incydenty

  • [ ] Wdrożono mechanizm logowania zdarzeń bezpieczeństwa
  • [ ] Zdefiniowano procedury reagowania na incydenty bezpieczeństwa
  • [ ] Regularne testy penetracyjne są przeprowadzane
  • [ ] Mechanizmy monitorowania i alertowania są wdrożone

Podsumowanie

Bezpieczeństwo aplikacji internetowych, w tym aplikacji Next.js, wymaga kompleksowego podejścia i ciągłej uwagi. W tym module omówiliśmy najważniejsze zagrożenia zdefiniowane w OWASP Top 10 oraz sposób ich minimalizacji w kontekście aplikacji Next.js.

Kluczowe punkty do zapamiętania:

  1. Kompleksowe podejście - bezpieczeństwo powinno być uwzględniane na każdym etapie tworzenia aplikacji
  2. Zasada najmniejszych uprawnień - przydzielaj minimalne uprawnienia niezbędne do wykonania zadania
  3. Walidacja i sanityzacja - zawsze waliduj i sanityzuj dane wejściowe
  4. Aktualizacje i patche - regularnie aktualizuj zależności i stosuj patche bezpieczeństwa
  5. Monitorowanie i logowanie - implementuj rozbudowane mechanizmy monitorowania i logowania
  6. Testowanie - regularnie przeprowadzaj testy bezpieczeństwa

Pamiętaj, że bezpieczeństwo to proces, nie produkt. Wymaga ciągłego doskonalenia i dostosowywania do nowych zagrożeń.

W następnym module przyjrzymy się audytowi i monitoringowi bezpieczeństwa, aby lepiej zrozumieć, jak wykrywać i reagować na potencjalne incydenty bezpieczeństwa.

Przejdź do CodeWorlds