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.
Przyjrzymy się, jak zabezpieczyć aplikacje Next.js przed najpoważniejszymi zagrożeniami z listy OWASP Top 10:
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:
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:
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:
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:
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:
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:
npm audit lub używanie narzędzi jak Snyk, DependabotZagroż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:
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:
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:
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:
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 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.0Poniżej przedstawiono listę kontrolną, którą możesz wykorzystać do oceny bezpieczeństwa swojej aplikacji Next.js:
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:
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.