Kurs Next.js · Moduł 7: Uwierzytelnianie i autoryzacja
Bezpieczeństwo aplikacji i OWASP
W tej lekcji5
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:
- Implementuj kontrolę dostępu na poziomie serwera, nigdy nie polegaj wyłącznie na zabezpieczeniach po stronie klienta
- Stosuj zasadę domyślnej odmowy (deny by default)
- Regularnie przeglądaj i testuj mechanizmy kontroli dostępu
- 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:
- Zawsze używaj HTTPS dla wszystkich stron aplikacji
- Wdrażaj odpowiednie nagłówki HTTP związane z bezpieczeństwem (HSTS, CSP)
- Regularnie aktualizuj biblioteki kryptograficzne
- Wdrażaj strategię rotacji kluczy kryptograficznych
- 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:
- Filtruj i sanityzuj dane wejściowe i wyjściowe
- Stosuj złożone, randomizowane hasła i identyfikatory sesji
- Implementuj timeout sesji i ograniczaj liczbę prób logowania
- Escape wszelkie dane przed umieszczeniem ich w poleceniach SQL, skryptach, czy HTML
- 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 uprawnień 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:
- Implementuj mechanizm wielopoziomowej ochrony (defense in depth)
- Przeprowadzaj modelowanie zagrożeń na wczesnym etapie tworzenia aplikacji
- Projektuj z myślą o zasadzie najmniejszych uprawnień (principle of least privilege)
- Traktuj dane użytkownika jako niezaufane i zawsze je waliduj
- 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:
- Usuwaj lub wyłączaj niepotrzebne funkcje, komponenty i zależności
- Regularnie aktualizuj wszystkie komponenty aplikacji i zależności
- Implementuj mechanizm audytowania konfiguracji
- Nigdy nie umieszczaj wrażliwych danych w kodzie źródłowym lub plikach dostępnych publicznie
- 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:
- Regularne wykonywanie
npm auditlub używanie narzędzi jak Snyk, Dependabot - Usuwanie nieużywanych zależności
- Subscrypcja na powiadomienia o lukach bezpieczeństwa w używanych bibliotekach
- Stosowanie zaufanych, dobrze utrzymywanych bibliotek z aktywną społecznością
- 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:
- Implementuj silne wymagania dotyczące haseł
- Wymagaj uwierzytelniania wieloskładnikowego (MFA) dla kont administratora i użytkowników z wysokimi uprawnieniami
- Nie ujawniaj szczegółowych informacji o błędach uwierzytelniania
- Implementuj bezpieczne mechanizmy odzyskiwania haseł
- 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:
- Weryfikuj integralność plików i kodów źródłowych w procesie CI/CD
- Weryfikuj pochodzenie zewnętrznych bibliotek i komponentów
- Używaj mechanizmów SRI (Subresource Integrity) dla zewnętrznych zasobów
- Implementuj automatyczne testy bezpieczeństwa w procesie CI/CD
- 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:
- Implementuj centralizowany system logowania i monitorowania
- Konfiguruj automatyczne alertowanie o podejrzanych zdarzeniach
- Zapewnij mechanizmy korelacji zdarzeń z różnych części aplikacji
- Regularnie analizuj logi pod kątem nieautoryzowanych działań
- 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]|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 // 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:
- Implementuj listy dozwolonych domen zamiast list blokowanych
- Używaj zapory sieciowej (firewall) do blokowania połączeń do wewnętrznych zasobów
- Filtruj i sanityzuj dane wejściowe zawierające URL
- Używaj minimalnych uprawnień dla kont serwisowych i aplikacji
- 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.0Lista 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:
- Kompleksowe podejście - bezpieczeństwo powinno być uwzględniane na każdym etapie tworzenia aplikacji
- Zasada najmniejszych uprawnień - przydzielaj minimalne uprawnienia niezbędne do wykonania zadania
- Walidacja i sanityzacja - zawsze waliduj i sanityzuj dane wejściowe
- Aktualizacje i patche - regularnie aktualizuj zależności i stosuj patche bezpieczeństwa
- Monitorowanie i logowanie - implementuj rozbudowane mechanizmy monitorowania i logowania
- 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.
Kod do tej lekcji: 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: 'Brak kontroli dostepu - uzytkownicy dzialaja poza uprawnieniami.',
17 example: 'GET /api/users/123/data - brak weryfikacji tożsamosci',
18 prevention: ['Domyslnie odmawiaj dostepu', 'Implementuj RBAC', 'Waliduj po stronie serwera'] },
19 { id: '2', name: 'Cryptographic Failures', owaspId: 'A02:2021', severity: 'critical',
20 description: 'Slabe szyfrowanie lub brak szyfrowania danych.',
21 example: 'Hasla w postaci jawnej w bazie danych',
22 prevention: ['bcrypt/argon2 do hasel', 'HTTPS', 'Nie przechowuj niepotrzebnych danych'] },
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 (bez parametryzacji)',
26 prevention: ['Parametryzowane zapytania', 'Walidacja danych', 'Uzywaj ORM'] },
27 { id: '4', name: 'Insecure Design', owaspId: 'A04:2021', severity: 'high',
28 description: 'Brak wzorcow bezpieczenstwa w architekturze.',
29 example: 'Brak rate limiting na logowaniu',
30 prevention: ['Modelowanie zagrozen', 'Wzorce bezpieczenstwa', 'Security code review'] },
31 { id: '5', name: 'Security Misconfiguration', owaspId: 'A05:2021', severity: 'high',
32 description: 'Domyslne ustawienia, otwarte porty, debug w produkcji.',
33 example: 'Szczegolowe bledy w produkcji',
34 prevention: ['Hardening', 'Minimalne uprawnienia', 'Audyty konfiguracji'] },
35 { id: '6', name: 'Vulnerable Components', owaspId: 'A06:2021', severity: 'medium',
36 description: 'Biblioteki z znanymi podatnosciami.',
37 example: 'Stara wersja lodash z prototype pollution',
38 prevention: ['npm audit', 'Aktualizuj zaleznosci', 'Monitoruj CVE'] },
39 { id: '7', name: 'Auth Failures', owaspId: 'A07:2021', severity: 'critical',
40 description: 'Slabe hasla, brak MFA, podatne tokeny.',
41 example: 'Brak limitu prob logowania',
42 prevention: ['MFA', 'Silne polityki hasel', 'Rate limiting'] },
43 { id: '8', name: 'Data Integrity', owaspId: 'A08:2021', severity: 'high',
44 description: 'Bledy integralnosci oprogramowania i danych.',
45 example: 'CI/CD bez weryfikacji artefaktow',
46 prevention: ['Podpisywanie artefaktow', 'Weryfikacja integralnosci', 'Bezpieczny CI/CD'] },
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} zabezpieczonych</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' }}>Przyklad:</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' }}>Zapobieganie:</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;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Multi-Factor Authentication (MFA) dodaje:
2. CSRF attacks można zapobiec przez:
To 2 z 6 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Układanie w pionie
Ułóż składnię importu i użycia redirect
- Klikanie w kolejności
Ułóż strukturę JWT tokena:
- Układanie w pionie
Ułóż elementy w prawidłowej kolejności: Track requests → Check limits → Block/Al
- Układanie w pionie
Uporządkuj cykl życia tokena JWT: