04 - Bezpieczne uwierzytelnianie: sesja, pliki cookie i nowoczesna tożsamość
Uwierzytelnianie i brama wejściowa Twojej aplikacji. Jeśli jest podatny na ataki, wszelkie inne środki bezpieczeństwa
można obejść. Według Verizon DBIR w 2024 r 74% naruszeń danych dotyczyło danych uwierzytelniających
skompromitowany lub ataki związane z tożsamością. Jednak większość aplikacji nadal jest wdrażana
Wzór uwierzytelniania 2010: sesje po stronie serwera z niechronionymi plikami cookie, hasła w postaci zwykłego tekstu w logach,
brak przechowywanych tokenów MFA i JWT localStorage.
Krajobraz uwierzytelniania w 2025 r. radykalnie się zmienił. Skonsolidowane najlepsze praktyki protokołu OAuth 2.1 bezpieczeństwo poprzez uczynienie PKCE obowiązkowym dla wszystkich klientów. Klucze oparte na WebAuthn zastępują tradycyjne hasła, a Apple, Google i Microsoft promują je jako standard. NIST SP 800-63B zmienione wytyczne dotyczące haseł poprzez wyeliminowanie wymagań dotyczących okresowej rotacji. Ten artykuł Ci pomoże przeprowadzi Cię przez całą powierzchnię ataku uwierzytelniającego, przedstawiając praktyczny kod, typowe pułapki i lista kontrolna bezpieczeństwa dla aplikacji Node.js.
Czego się nauczysz
- Bezpieczne zarządzanie sesją: pliki cookie HttpOnly, Secure, SameSite i zapobieganie utrwalaniu sesji
- Najlepsze praktyki JWT i 5 fatalnych błędów, które sprawiają, że tokeny są niepewne
- OAuth 2.1 z PKCE: przepływ kodu autoryzacyjnego dla SPA i aplikacji mobilnych
- WebAuthn i hasła: Praktyczne wdrożenie z SimpleWebAuthn
- MFA z TOTP: Integracja z Google Authenticator i Authy poprzez otpauth
- RBAC, ABAC i ReBAC: modele autoryzacji dla nowoczesnych aplikacji
- Pełne oprogramowanie pośredniczące do uwierzytelniania Express.js z ograniczeniem szybkości i dziennikiem audytu
- Lista kontrolna OWASP A07:2021 (Błędy identyfikacji i uwierzytelnienia)
Zarządzanie sesją: podstawy
Zarządzanie sesjami i mechanizm, za pomocą którego serwer „zapamiętuje” uwierzytelnionego użytkownika Bezstanowe żądania HTTP. Bezpieczna sesja wymaga czterech podstawowych właściwości: nieprzewidywalne identyfikatory, bezpieczna transmisja, kontrolowane wygaśnięcie e prawidłowe unieważnienie przy wylogowaniu.
Głównym wektorem ataku jest utrwalanie sesji: Atakujący zapewnia ofiarę
znany identyfikator sesji przed zalogowaniem, a następnie ponownie wykorzystuje ten sam identyfikator po uwierzytelnieniu, aby się podszywać
użytkownik. Rozwiązaniem jest zawsze ponowne generowanie identyfikatora sesji po zalogowaniu. Drugi wektor to
przejmowanie sesji za pośrednictwem XSS, ograniczane przez oznaczone pliki cookie HttpOnly. Trzeci
wektory to sesje dla sierot: Po stronie serwera nigdy nie unieważniano pozostałych sesji
aktywne nawet po usunięciu przez klienta pliku cookie, umożliwiając odtworzenie skradzionych identyfikatorów sesji.
// Session management sicuro con express-session
// npm install express-session connect-pg-simple @types/express-session
import session from 'express-session';
import pgSession from 'connect-pg-simple';
import crypto from 'crypto';
const PgSession = pgSession(session);
// Configurazione sicura della sessione
app.use(session({
// Store PostgreSQL invece di MemoryStore (non usare in produzione)
store: new PgSession({
pool: dbPool,
tableName: 'user_sessions',
createTableIfMissing: true,
}),
// Secret forte: usa variabile d'ambiente, mai hardcoded
secret: process.env.SESSION_SECRET!, // min 32 caratteri random
// Non salvare sessioni non modificate
resave: false,
// Non creare sessioni vuote per utenti non autenticati
saveUninitialized: false,
// Configurazione cookie sicura
cookie: {
httpOnly: true, // Blocca accesso JavaScript (XSS protection)
secure: true, // Solo HTTPS (usa false solo in sviluppo locale)
sameSite: 'strict', // Blocca CSRF cross-origin
maxAge: 8 * 60 * 60 * 1000, // 8 ore in millisecondi
domain: process.env.COOKIE_DOMAIN, // Limita al dominio specifico
path: '/',
},
// Nome custom invece di 'connect.sid' (riduce fingerprinting)
name: '__session',
// Genera ID sicuro con crypto.randomBytes
genid: () => crypto.randomBytes(32).toString('hex'),
}));
// CRITICO: Rigenera session ID dopo login (previene session fixation)
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticateUser(username, password);
if (!user) {
return res.status(401).json({ error: 'Credenziali non valide' });
}
// CRITICO: rigenerare SEMPRE il session ID dopo autenticazione
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.userRole = user.role;
req.session.loginTime = Date.now();
req.session.save((err) => {
if (err) return res.status(500).json({ error: 'Session save error' });
res.json({ success: true });
});
});
});
// Logout sicuro: distruggi sessione lato server
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Logout error' });
// Elimina il cookie dal browser
res.clearCookie('__session', {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
res.json({ success: true });
});
});
Anty-wzorzec: MemoryStore w produkcji
Nigdy nie używaj domyślnego magazynu pamięci sesji ekspresowej w środowisku produkcyjnym.
Powoduje wyciek pamięci, nie skaluje się w poziomie (sesje tracone przy ponownym uruchomieniu węzła),
i nie utrzymuje sesji pomiędzy wdrożeniami. Zawsze korzystaj ze sklepu zewnętrznego: Redis
(connect-redis), PostgreSQL (connect-pg-simple),
lub MongoDB (connect-mongo). Redis jest preferowanym wyborem ze względu na wydajność
optymalny i automatyczny TTL wygasłych kluczy.
Bezpieczeństwo plików cookie: flagi, które mają znaczenie
Sesyjne pliki cookie muszą być skonfigurowane z co najmniej trzema atrybutami bezpieczeństwa. Każdy łagodzi określoną kategorię ataków. Prawidłowa kombinacja to HttpOnly + Secure + SameSite, skonfigurowane razem dla ochrony warstwowej:
| Atrybut | Zalecana wartość | Zagrożenie złagodzone |
|---|---|---|
HttpOnly |
true |
XSS: JavaScript nie może odczytać pliku cookie |
Secure |
true |
MITM: plik cookie przesyłany wyłącznie za pośrednictwem protokołu HTTPS |
SameSite |
Strict o Lax |
CSRF: Blokuj nieautoryzowane przesyłanie danych z różnych źródeł |
MaxAge |
8-24 godziny w przypadku normalnych sesji | Sesje osierocone: automatyczne wygaśnięcie po stronie przeglądarki |
Domain |
Specyficzne dla domeny, bez symboli wieloznacznych | Wyciek plików cookie w zaatakowanych subdomenach |
Wybór pomiędzy SameSite=Strict e SameSite=Lax To zależy od przypadku użycia.
Ścisły zapewnia maksymalną ochronę, ale blokuje pliki cookie nawet wtedy, gdy użytkownik przegląda
do Twojej witryny z zewnętrznego linku (np. e-maila lub innej witryny), co powoduje każdorazowe przekierowanie w celu zalogowania.
Niedbały zezwala na plik cookie w nawigacji najwyższego poziomu (kliknij na link), ale blokuje go
Żądania między źródłami POST/PUT/DELETE zapewniają dobrą równowagę między bezpieczeństwem a użytecznością.
W przypadku interfejsów API używanych przez SPA w innej domenie użyj SameSite=None; Secure z ochroną
CSRF poprzez token w nagłówku.
Najlepsze praktyki JWT i 5 fatalnych błędów
Tokeny internetowe JSON to potężne narzędzie do bezstanowego uwierzytelniania w interfejsach API, ale ich nieprawidłowa implementacja i jedna z najczęstszych przyczyn krytycznych podatności. NIST i OWASP zidentyfikować pięć błędów krytycznych, z których każdy całkowicie unieważnia bezpieczeństwo JWT z niszczycielskimi konsekwencjami, jeśli zostaną wykorzystane w produkcji.
5 fatalnych błędów JWT
- Błąd nr 1 – algorytm „żaden”: Niektóre serwery akceptują tokeny za pomocą
"alg": "none", bez podpisu. Osoba atakująca może sfałszować dowolny ładunek. - Błąd nr 2 – Pomieszanie algorytmów: Serwer RS256 obsługujący standard HS256 umożliwia atakującemu podpisanie się przy użyciu (znanego) klucza publicznego.
- Błąd nr 3 — JWT w localStorage: Dostępne z dowolnego skryptu JS. XSS na dowolnej zależności npm może ukraść token.
- Błąd nr 4 – Tokeny nie wygasają: Token dostępu, który nigdy nie wygasa, nie może zostać unieważniony w przypadku naruszenia bezpieczeństwa.
- Błąd nr 5 – Słaby sekret dla HS256: Krótkie lub przewidywalne sekrety są podatne na ataki słownikowe offline lub ataki siłowe.
// JWT sicuro con jsonwebtoken - Node.js TypeScript
// npm install jsonwebtoken @types/jsonwebtoken
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
const JWT_CONFIG = {
// Usa RS256 o EdDSA per produzione, non HS256 se il secret può trapelare
// HS256 va bene solo se il secret e condiviso tra servizi fidati interni
algorithm: 'RS256' as const,
accessTokenExpiry: '15m', // Token di accesso: vita breve (15 minuti)
refreshTokenExpiry: '7d', // Refresh token: vita lunga, salvato in DB
};
// Carica chiave privata RSA da variabile d'ambiente
const PRIVATE_KEY = process.env.JWT_PRIVATE_KEY!;
const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY!;
// Genera access token
function generateAccessToken(userId: string, role: string): string {
return jwt.sign(
{
sub: userId, // Subject: ID utente
role: role,
iat: Math.floor(Date.now() / 1000),
jti: crypto.randomUUID(), // JWT ID univoco (per blacklist/revoca)
},
PRIVATE_KEY,
{
algorithm: JWT_CONFIG.algorithm,
expiresIn: JWT_CONFIG.accessTokenExpiry,
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
}
);
}
// Verifica token - SPECIFICA SEMPRE l'algoritmo atteso
function verifyAccessToken(token: string): jwt.JwtPayload {
try {
return jwt.verify(token, PUBLIC_KEY, {
// CRITICO: whitelist di algoritmi, mai lasciare aperto o accettare "none"
algorithms: ['RS256'],
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
}) as jwt.JwtPayload;
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
throw new Error('TOKEN_EXPIRED');
}
if (err instanceof jwt.JsonWebTokenError) {
throw new Error('TOKEN_INVALID');
}
throw err;
}
}
// Refresh token rotation: ogni uso genera un nuovo refresh token
// Salva il refresh token nel DB con hash bcrypt (non in chiaro)
import bcrypt from 'bcrypt';
async function storeRefreshToken(
userId: string,
token: string
): Promise<void> {
const tokenHash = await bcrypt.hash(token, 12);
await db.query(
`INSERT INTO refresh_tokens (user_id, token_hash, expires_at, created_at)
VALUES ($1, $2, NOW() + INTERVAL '7 days', NOW())`,
[userId, tokenHash]
);
}
// Middleware di autenticazione Express
export const authenticateJWT = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
// Leggi token da cookie HttpOnly (preferibile) o Authorization header
// EVITA localStorage: vulnerabile a XSS
const token = req.cookies['access_token'] ?? extractBearerToken(req);
if (!token) {
res.status(401).json({ error: 'Authentication required' });
return;
}
try {
const payload = verifyAccessToken(token);
req.user = { id: payload.sub!, role: payload.role };
next();
} catch (err) {
if ((err as Error).message === 'TOKEN_EXPIRED') {
res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
} else {
res.status(401).json({ error: 'Invalid token' });
}
}
};
function extractBearerToken(req: Request): string | null {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.slice(7);
}
return null;
}
Pamięć JWT: Cookie HttpOnly vs localStorage
Wiele samouczków zaleca zapisywanie JWT w localStorage ponieważ jest prosty w użyciu
ze SPA. Nigdy nie rób tego w przypadku tokenów uwierzytelniających. localStorage i dostępne
z dowolnego skryptu JavaScript działającego na stronie, w tym z zaatakowanych bibliotek stron trzecich.
Atak XSS na dowolną zależność npm może po cichu ukraść wszystkie tokeny.
Prawidłowym rozwiązaniem jest użycie plik cookie HttpOnly + Secure + SameSite=Strict:
niedostępne przez JavaScript, przesyłane tylko poprzez HTTPS i z wbudowaną ochroną CSRF.
W przypadku SPA, które muszą wysyłać żądania API z różnych źródeł, użyj SameSite=None; Secure
z tokenem CSRF w nagłówku żądania.
OAuth 2.1 z PKCE: bezpieczne uwierzytelnianie federacyjne
OAuth 2.1 (wersja robocza RFC, skonsolidowana w 2024 r.) ujednolica najlepsze praktyki w zakresie bezpieczeństwa zgromadzone przez lata wdrożenia protokołu OAuth 2.0, renderowania Wymagany PKCE (Proof Key for Code Exchange). dla wszystkich klientów, w tym klientów poufnych posiadających tajemnice klientów. Eliminuje także ukryty przepływ i przyznanie hasła, które są uważane za z natury niebezpieczne i przestarzałe.
Przepływ kodu autoryzacyjnego z PKCE działa w następujący sposób: klient generuje plik weryfikator_kodu losowy (43-128 znaków), oblicza kod_wyzwanie jak SHA-256 Base64URL, wysyła wyzwanie do serwera autoryzacyjnego i po otrzymaniu kodu autoryzacyjnego dokonuje jego wymiany z tokenem za okazaniem oryginału weryfikatora. Nawet jeśli atakujący przechwyci kod podczas przekierowania, nie można go wymienić bez weryfikatora, który pozostaje tajny w kliencie.
// OAuth 2.1 PKCE flow - Client SPA TypeScript
interface PKCEChallenge {
codeVerifier: string;
codeChallenge: string;
state: string;
}
// Genera PKCE challenge e state anti-CSRF
async function generatePKCEChallenge(): Promise<PKCEChallenge> {
// code_verifier: stringa random 64 caratteri (range accettato: 43-128)
const codeVerifier = generateRandomString(64);
// code_challenge = BASE64URL(SHA256(ASCII(code_verifier)))
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
// state: token anti-CSRF univoco per questa richiesta di autorizzazione
const state = generateRandomString(32);
return { codeVerifier, codeChallenge, state };
}
function generateRandomString(length: number): string {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return Array.from(array, (byte) =>
byte.toString(36).padStart(2, '0')
).join('').slice(0, length);
}
// Avvia il flusso OAuth 2.1 con redirect all'authorization server
async function startOAuthFlow(): Promise<void> {
const { codeVerifier, codeChallenge, state } = await generatePKCEChallenge();
// Salva temporaneamente in sessionStorage (non localStorage)
// sessionStorage viene cancellato alla chiusura del tab: meno rischio di leak
sessionStorage.setItem('pkce_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: process.env['VITE_OAUTH_CLIENT_ID']!,
redirect_uri: `${window.location.origin}/auth/callback`,
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256', // Sempre S256, mai 'plain'
state: state,
});
window.location.href = `${AUTH_SERVER_URL}/authorize?${params}`;
}
// Callback handler: scambia authorization code con access token
async function handleOAuthCallback(): Promise<void> {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const returnedState = params.get('state');
const error = params.get('error');
if (error) {
throw new Error(`OAuth error: ${error}: ${params.get('error_description')}`);
}
// Verifica state anti-CSRF prima di procedere
const savedState = sessionStorage.getItem('oauth_state');
if (!code || returnedState !== savedState) {
throw new Error('Invalid OAuth callback: state mismatch (possibile attacco CSRF)');
}
const codeVerifier = sessionStorage.getItem('pkce_verifier');
if (!codeVerifier) {
throw new Error('Missing PKCE verifier');
}
// Pulizia immediata dei dati temporanei
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
// Scambia authorization code con access token
const response = await fetch(`${AUTH_SERVER_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env['VITE_OAUTH_CLIENT_ID']!,
redirect_uri: `${window.location.origin}/auth/callback`,
code: code,
code_verifier: codeVerifier, // Prova che siamo noi i legittimi richiedenti
}),
});
if (!response.ok) {
const body = await response.json();
throw new Error(`Token exchange failed: ${body.error}`);
}
const tokens = await response.json();
// Passa i token al backend via endpoint sicuro per setting cookie HttpOnly
await securelyStoreTokens(tokens);
}
WebAuthn i Passkeys: uwierzytelnianie bez hasła
Klucze oparte na WebAuthn (Web Authentication API, standard W3C) reprezentują zmianę najbardziej znaczący w uwierzytelnianiu w ciągu ostatnich dwudziestu lat. W przeciwieństwie do haseł, klucze stosują asymetryczną kryptografię klucza publicznego: klucz prywatny pozostaje na urządzeniu użytkownika chronione biometrią lub PIN-em urządzenia, podczas gdy serwer zna jedynie klucz publiczny.
Wynik jest jeden absolutna odporność na phishing (klucz jest kryptograficzny połączony z oryginalną domeną), brak haseł do kradzieży w przypadku naruszeń bazy danych i doskonały UX Face ID lub Touch ID zamiast hasła + 2FA. W 2025 roku dalej 15 miliardów kont klucze wsparcia. Biblioteka SimpleWebAuthn ogromnie upraszcza wdrożenie.
// WebAuthn / Passkeys con SimpleWebAuthn - Node.js Backend
// npm install @simplewebauthn/server @simplewebauthn/browser @simplewebauthn/types
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import type {
RegistrationResponseJSON,
AuthenticationResponseJSON,
} from '@simplewebauthn/types';
const RP_NAME = 'MyApp';
const RP_ID = process.env.RP_ID ?? 'myapp.com'; // Il tuo dominio (no https://)
const ORIGIN = `https://${RP_ID}`;
// STEP 1: Registrazione - genera challenge per il client
app.post('/auth/webauthn/register/start', requireAuth, async (req, res) => {
const user = req.user!;
// Recupera passkeys esistenti per escluderle (no duplicati)
const existingPasskeys = await getPasskeysByUser(user.id);
const options = await generateRegistrationOptions({
rpName: RP_NAME,
rpID: RP_ID,
userName: user.email,
userDisplayName: user.name,
excludeCredentials: existingPasskeys.map((pk) => ({
id: pk.credentialId,
})),
authenticatorSelection: {
// 'platform': usa Face ID/Touch ID/Windows Hello (passkey nativa)
// 'cross-platform': usa YubiKey/chiavi hardware esterne
authenticatorAttachment: 'platform',
residentKey: 'required', // Passkey discoverable: login senza username
userVerification: 'required', // Richiedi biometrico o PIN obbligatoriamente
},
attestationType: 'none', // 'direct' per scenari enterprise con audit
});
// Salva challenge in sessione (valido per una sola verifica, poi cancellato)
req.session.currentChallenge = options.challenge;
res.json(options);
});
// STEP 2: Registrazione - verifica risposta del client
app.post('/auth/webauthn/register/verify', requireAuth, async (req, res) => {
const body: RegistrationResponseJSON = req.body;
const expectedChallenge = req.session.currentChallenge;
if (!expectedChallenge) {
return res.status(400).json({ error: 'No challenge in session' });
}
try {
const { verified, registrationInfo } = await verifyRegistrationResponse({
response: body,
expectedChallenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
requireUserVerification: true,
});
if (!verified || !registrationInfo) {
return res.status(400).json({ error: 'Verification failed' });
}
// Salva le credenziali nel database
await savePasskey({
userId: req.user!.id,
credentialId: registrationInfo.credential.id,
publicKey: Buffer.from(registrationInfo.credential.publicKey),
counter: registrationInfo.credential.counter,
deviceType: registrationInfo.credentialDeviceType,
backedUp: registrationInfo.credentialBackedUp,
});
req.session.currentChallenge = undefined;
res.json({ verified: true });
} catch (err) {
res.status(400).json({ error: 'Registration verification failed' });
}
});
// STEP 3: Login con passkey - genera challenge di autenticazione
app.post('/auth/webauthn/authenticate/start', async (req, res) => {
const { email } = req.body;
const user = await getUserByEmail(email);
if (!user) {
// Non rivelare se l'utente esiste (user enumeration prevention)
return res.status(400).json({ error: 'Authentication failed' });
}
const passkeys = await getPasskeysByUser(user.id);
const options = await generateAuthenticationOptions({
rpID: RP_ID,
allowCredentials: passkeys.map((pk) => ({ id: pk.credentialId })),
userVerification: 'required',
});
req.session.currentChallenge = options.challenge;
req.session.pendingUserId = user.id;
res.json(options);
});
// STEP 4: Verifica autenticazione e crea sessione
app.post('/auth/webauthn/authenticate/verify', async (req, res) => {
const body: AuthenticationResponseJSON = req.body;
const expectedChallenge = req.session.currentChallenge;
const userId = req.session.pendingUserId;
if (!expectedChallenge || !userId) {
return res.status(400).json({ error: 'Invalid session state' });
}
const passkey = await getPasskeyByCredentialId(body.id);
if (!passkey || passkey.userId !== userId) {
return res.status(400).json({ error: 'Passkey not found' });
}
try {
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response: body,
expectedChallenge,
expectedOrigin: ORIGIN,
expectedRPID: RP_ID,
credential: {
id: passkey.credentialId,
publicKey: passkey.publicKey,
counter: passkey.counter, // Verifica replay attack tramite counter crescente
},
});
if (!verified) {
return res.status(401).json({ error: 'Authentication failed' });
}
// Aggiorna counter per replay attack prevention
await updatePasskeyCounter(passkey.credentialId, authenticationInfo.newCounter);
// Crea sessione autenticata rigenerando il session ID
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = userId;
req.session.currentChallenge = undefined;
req.session.pendingUserId = undefined;
res.json({ verified: true });
});
} catch (err) {
res.status(401).json({ error: 'Authentication verification failed' });
}
});
MSZ i TOTP: czynnik drugi
Uwierzytelnianie wieloskładnikowe (MFA) to najskuteczniejszy środek zaradczy przeciwko upychaniu poświadczeń i klasyczny phishing oparty na hasłach. Według Microsoftu MFA blokuje 99,9% automatyczne ataki na konta. TOTP (hasło jednorazowe oparte na czasie, RFC 6238) i lo de facto standard oprogramowania drugiego stopnia, kompatybilny z Google Authenticator, Authy, 1Password i dowolna aplikacja zgodna z TOTP.
Często pomijany krytyczny aspekt: przepływ MFA musi być atomowe i nie da się ich obejść.
Nigdy nie twórz częściowo uwierzytelnionej sesji po pierwszym czynniku, który na to pozwala
jakąkolwiek operację. Zamiast tego użyj sesji tymczasowej z flagą mfaPending: true,
i przyznaj pełny dostęp dopiero po zweryfikowaniu drugiego czynnika.
// MFA TOTP implementation - Node.js
// npm install otpauth qrcode
// NOTA: speakeasy non e più mantenuto dal 2017, usa 'otpauth' invece
import * as OTPAuth from 'otpauth';
import QRCode from 'qrcode';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
// STEP 1: Abilita 2FA - genera secret e QR code per l'utente
app.post('/auth/2fa/setup', requireAuth, async (req, res) => {
const user = req.user!;
// Genera nuovo secret TOTP con randomness crittografico
const totp = new OTPAuth.TOTP({
issuer: 'MyApp',
label: user.email,
algorithm: 'SHA1', // SHA1 e lo standard TOTP (RFC 6238)
digits: 6,
period: 30, // 30 secondi per OTP (standard)
secret: OTPAuth.Secret.fromRandom(20), // 160 bit di entropia
});
// Salva secret temporaneo (non confermato ancora)
const secretBase32 = totp.secret.base32;
await savePending2FASecret(user.id, secretBase32);
// Genera QR code per l'app authenticator
const otpAuthUrl = totp.toString();
const qrCodeDataUrl = await QRCode.toDataURL(otpAuthUrl);
res.json({
secret: secretBase32, // Mostra per inserimento manuale
qrCode: qrCodeDataUrl, // URL del QR code (base64 data URL)
});
});
// STEP 2: Verifica e conferma attivazione 2FA
app.post('/auth/2fa/verify-setup', requireAuth, async (req, res) => {
const { token } = req.body;
const user = req.user!;
const pendingSecret = await getPending2FASecret(user.id);
if (!pendingSecret) {
return res.status(400).json({ error: '2FA setup not initiated' });
}
const totp = new OTPAuth.TOTP({
secret: OTPAuth.Secret.fromBase32(pendingSecret),
algorithm: 'SHA1',
digits: 6,
period: 30,
});
// Valida il codice con window=1 (accetta ±30 secondi per clock skew)
const delta = totp.validate({ token, window: 1 });
if (delta === null) {
return res.status(400).json({ error: 'Codice TOTP non valido' });
}
// Genera codici di recovery monouso
const recoveryCodes = generateRecoveryCodes(8);
const hashedCodes = await Promise.all(
recoveryCodes.map((code) => bcrypt.hash(code, 12))
);
// Attiva 2FA: salva secret definitivo e recovery codes hashati
await enable2FA(user.id, pendingSecret, hashedCodes);
await deletePending2FASecret(user.id);
// I recovery codes in chiaro vengono mostrati UNA SOLA VOLTA
res.json({ success: true, recoveryCodes });
});
// STEP 3: Login con 2FA - verifica secondo fattore
app.post('/auth/2fa/challenge', async (req, res) => {
const { token } = req.body;
// Verifica stato sessione: deve essere in pending MFA
if (!req.session.mfaPending || !req.session.pendingUserId) {
return res.status(401).json({ error: 'Invalid session state' });
}
const user = await getUserById(req.session.pendingUserId);
if (!user?.totpSecret) {
return res.status(400).json({ error: '2FA not configured' });
}
const totp = new OTPAuth.TOTP({
secret: OTPAuth.Secret.fromBase32(user.totpSecret),
algorithm: 'SHA1',
digits: 6,
period: 30,
});
const delta = totp.validate({ token, window: 1 });
if (delta === null) {
// Rate limiting: incrementa tentativi falliti per questo account
await incrementFailedMFAAttempts(user.id);
return res.status(401).json({ error: 'Codice 2FA non valido' });
}
// TOTP valido: promuovi a sessione completamente autenticata
await resetFailedMFAAttempts(user.id);
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.mfaPending = false;
req.session.pendingUserId = undefined;
res.json({ success: true });
});
});
// Genera recovery codes in formato leggibile (es. ABCD-EFGH-IJKL)
function generateRecoveryCodes(count: number): string[] {
return Array.from({ length: count }, () =>
Array.from({ length: 3 }, () =>
crypto.randomBytes(2).toString('hex').toUpperCase()
).join('-')
);
}
Autoryzacja: RBAC, ABAC i ReBAC
Uwierzytelnienie (kim jesteś?) i autoryzacja (co możesz zrobić?) to różne pojęcia, ale często zaimplementowane razem w tym samym oprogramowaniu pośrednim, tworząc kod trudny do testowania i utrzymania. Po zweryfikowaniu tożsamości system musi zdecydować, czy użytkownik ma do tego uprawnienia wymagana konkretna operacja. Istnieją trzy główne modele:
RBAC (kontrola dostępu oparta na rolach) przypisywać uprawnienia do predefiniowanych ról i ról użytkownikom. Jest to najprostszy model do wdrożenia i odpowiedni dla większości aplikacje. Limit i sztywność: przy wielu najemcach lub zasobach osobistych, liczba ról eksploduje.
ABAC (kontrola dostępu oparta na atrybutach) ocenić atrybuty użytkownika (rola, dział, poziom), zasobu (właściciel, klasyfikacja, najemca) i kontekst (czas, IP, urządzenie). Bardziej elastyczny niż RBAC, ale bardziej skomplikowany we wdrażaniu i debugowaniu.
ReBAC (kontrola dostępu oparta na relacjach) opiera uprawnienia na relacjach pomiędzy podmiotami na wykresie danych (np. Google Zanzibar). Używany przez Dysk Google, GitHub, Notion. Idealny do zastosowań o hierarchicznej strukturze własności.
// Middleware RBAC + ABAC ibrido - Node.js Express TypeScript
// Definizione dei permessi per ruolo
const ROLE_PERMISSIONS: Record<string, string[]> = {
admin: ['users:read', 'users:write', 'users:delete', 'posts:*'],
editor: ['posts:read', 'posts:write', 'posts:delete'],
viewer: ['posts:read', 'users:read'],
};
// RBAC Middleware: verifica permesso base del ruolo
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user?.role;
if (!userRole) {
return res.status(401).json({ error: 'Not authenticated' });
}
const permissions = ROLE_PERMISSIONS[userRole] ?? [];
const hasPermission = permissions.some((p) => {
if (p.endsWith(':*')) {
return permission.startsWith(p.slice(0, -1));
}
return p === permission;
});
if (!hasPermission) {
return res.status(403).json({ error: 'Permessi insufficienti' });
}
next();
};
}
// ABAC: verifica proprietà della risorsa (ownership check)
function requireOwnership<T extends { authorId: string }>(
getResource: (id: string) => Promise<T | null>
) {
return async (req: Request, res: Response, next: NextFunction) => {
const resourceId = req.params.id;
const resource = await getResource(resourceId);
if (!resource) {
return res.status(404).json({ error: 'Risorsa non trovata' });
}
// Admin bypassa il check di proprietà per operazioni di supporto
if (req.user?.role === 'admin') {
req.resource = resource;
return next();
}
// Gli altri devono essere proprietari della risorsa
if (resource.authorId !== req.user?.id) {
return res.status(403).json({ error: 'Accesso negato' });
}
req.resource = resource;
next();
};
}
// Composizione: RBAC + ABAC applicati in sequenza sulla stessa route
app.put(
'/api/posts/:id',
authenticateJWT, // 1. Autenticazione: chi sei?
requirePermission('posts:write'), // 2. RBAC: il tuo ruolo può scrivere posts?
requireOwnership(getPostById), // 3. ABAC: sei il proprietario di questo post?
async (req: Request, res: Response) => {
// req.resource e il post verificato e disponibile
const updatedPost = await updatePost(req.params.id, req.body);
res.json(updatedPost);
}
);
// Esempio con Casbin per sistemi multi-tenant più complessi
// npm install casbin
import { newEnforcer } from 'casbin';
const enforcer = await newEnforcer(
'rbac_with_domains_model.conf',
'policy.csv'
);
// Verifica permesso con contesto di dominio/tenant
async function checkTenantPermission(
userId: string,
tenantId: string,
resource: string,
action: string
): Promise<boolean> {
return enforcer.enforce(userId, tenantId, resource, action);
}
Bezpieczeństwo haseł: wytyczne dotyczące haszowania i NIST 2025
Wytyczne NIST SP 800-63B (wersja z 2024 r.) zasadniczo zmieniły podejście do haseł.
Pożegnanie z obowiązkową okresową rotacją (właściwie powoduje słabsze i bardziej przewidywalne hasła, takie jak
MyApp2024! co staje się MyApp2025!). Pożegnaj sztywne wymagania dotyczące charakteru
specjalne (użytkownicy używają przewidywalnych wzorców, takich jak P@ssw0rd1!). Nowe wytyczne
skupienie się na: minimalna długość 8 znaków, maksymalna długość co najmniej 64 znaki, porównanie
z naruszonymi bazami danych haseł i bez wymagań dotyczących obowiązkowego wybierania numerów.
bcrypt vs Argon2id: który wybrać?
- Argon2id (zalecane w przypadku nowych projektów): Zwycięzca konkursu hashowania haseł 2015. Odporny na ataki GPU (wymagające dużej pamięci) i ataki typu side-channel. Zalecane parametry: koszt pamięci 64 MB, koszt czasu 3, równoległość 4.
- szyfrować (odpowiednie dla starszych systemów): nadal bezpieczne przy współczynniku kosztów >= 12. Ostrzeżenie: dyskretnie obcinaj hasła dłuższe niż 72 znaki — zawsze używaj wstępnego skrótu lub opakowania, które obsługuje ten limit.
- skrypt: dobra alternatywa dla Argon2, ale bardziej złożona i mniej udokumentowana parametryzacja dla programistów.
- MD5, SHA-1, SHA-256 bez soli: NIGDY nie używaj jako hasła. Są to szybkie funkcje skrótu, a nie kluczowe funkcje wyprowadzania. Osoba atakująca posiadająca nowoczesny procesor graficzny może zweryfikować miliardy skrótów na sekundę.
// Password hashing sicuro con Argon2id + controllo HaveIBeenPwned
// npm install argon2
import argon2 from 'argon2';
const ARGON2_OPTIONS: argon2.Options = {
type: argon2.argon2id, // argon2id: bilanciamento ottimale sicurezza/performance
memoryCost: 64 * 1024, // 64 MB di RAM richiesta (difesa contro GPU farms)
timeCost: 3, // 3 iterazioni sequenziali
parallelism: 4, // 4 thread paralleli
saltLength: 32, // Salt da 32 byte (generato automaticamente da argon2)
};
// Hash password durante registrazione
async function hashPassword(password: string): Promise<string> {
if (password.length < 8) {
throw new Error('Password troppo corta (minimo 8 caratteri)');
}
if (password.length > 128) {
throw new Error('Password troppo lunga (massimo 128 caratteri)');
}
// Verifica contro HaveIBeenPwned prima di salvare
const isPwned = await checkHaveIBeenPwned(password);
if (isPwned) {
throw new Error(
'Questa password e stata trovata in un data breach. Scegli una password diversa.'
);
}
return argon2.hash(password, ARGON2_OPTIONS);
}
// Verifica password durante login (timing-safe grazie ad argon2.verify)
async function verifyPassword(
plainPassword: string,
hashedPassword: string
): Promise<boolean> {
try {
return await argon2.verify(hashedPassword, plainPassword);
} catch {
return false;
}
}
// Controllo HaveIBeenPwned con k-anonymity
// Non invia la password intera: solo i primi 5 caratteri dello SHA-1 hash
async function checkHaveIBeenPwned(password: string): Promise<boolean> {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
const hashHex = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
// Invia solo i primi 5 caratteri dell'hash (k-anonymity model)
const prefix = hashHex.slice(0, 5);
const suffix = hashHex.slice(5);
const response = await fetch(
`https://api.pwnedpasswords.com/range/${prefix}`,
{ headers: { 'Add-Padding': 'true' } } // Padding contro traffic analysis
);
const text = await response.text();
// Verifica se il nostro suffisso e nel risultato (>0 occorrenze)
return text.split('\n').some((line) =>
line.split(':')[0].trim() === suffix
);
}
Pełny stos uwierzytelniania: limitowanie szybkości i dziennik audytu
Gotowa do produkcji implementacja uwierzytelniania musi obejmować ograniczenie szybkości, aby temu zapobiec brutalna siła, blokada konta z powiadomieniem użytkownika i rejestrowanie audytu pod kątem zgodności z przepisami i reakcja na incydent. Oto kompletny wzór integrujący wszystkie warstwy:
// Login completo con tutti i layer di sicurezza - Express.js
// npm install express-rate-limit rate-limit-redis ioredis
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// Rate limiter per endpoint di login (brute force protection a livello IP)
export const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // Finestra di 15 minuti
max: 5, // Max 5 tentativi per IP per finestra
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({
sendCommand: (...args: string[]) => (redis as any).call(...args),
}),
handler: (req, res) => {
res.status(429).json({
error: 'Troppi tentativi di login. Riprova tra 15 minuti.',
retryAfter: res.getHeader('Retry-After'),
});
},
});
// Account lockout (distinto dal rate limiting per IP: blocca l'account specifico)
async function checkAccountLockout(email: string): Promise<void> {
const key = `lockout:${email}`;
const attempts = await redis.get(key);
if (attempts && parseInt(attempts) >= 10) {
throw new Error('ACCOUNT_LOCKED');
}
}
async function recordFailedLogin(email: string): Promise<void> {
const key = `lockout:${email}`;
const pipeline = redis.pipeline();
pipeline.incr(key);
pipeline.expire(key, 30 * 60); // Reset dopo 30 minuti
await pipeline.exec();
}
// Audit log per conformità e incident response
async function logAuthEvent(event: {
userId?: string;
email: string;
action: 'LOGIN' | 'LOGOUT' | 'REGISTER' | 'PASSWORD_CHANGE' | 'MFA_VERIFY';
success: boolean;
ip: string;
userAgent: string;
}): Promise<void> {
await db.query(
`INSERT INTO auth_audit_log
(user_id, email, action, success, ip_address, user_agent, created_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())`,
[event.userId, event.email, event.action, event.success, event.ip, event.userAgent]
);
}
// Endpoint di login: combina tutti i layer
app.post('/auth/login', loginRateLimiter, async (req: Request, res: Response) => {
const { email, password } = req.body;
const ip = req.ip ?? 'unknown';
const userAgent = req.get('User-Agent') ?? 'unknown';
if (!email || !password || typeof email !== 'string') {
return res.status(400).json({ error: 'Email e password richiesti' });
}
try {
await checkAccountLockout(email);
const user = await getUserByEmail(email.toLowerCase().trim());
// CRITICO: verifica la password SEMPRE, anche se l'utente non esiste
// Previene timing attack che rivelano se un utente e registrato o no
const passwordValid = user
? await verifyPassword(password, user.passwordHash)
: await verifyPassword(password, '$argon2id$v=19$m=65536,t=3,p=4$fake$fake');
if (!user || !passwordValid) {
await recordFailedLogin(email);
await logAuthEvent({ email, action: 'LOGIN', success: false, ip, userAgent });
// Risposta generica: non rivelare se l'utente esiste
return res.status(401).json({ error: 'Credenziali non valide' });
}
if (user.mfaEnabled) {
// Sessione parziale: blocca fino al completamento del secondo fattore
req.session.mfaPending = true;
req.session.pendingUserId = user.id;
return res.json({ mfaRequired: true });
}
// Login completo (senza MFA)
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.userRole = user.role;
logAuthEvent({ userId: user.id, email, action: 'LOGIN', success: true, ip, userAgent });
res.json({ success: true });
});
} catch (err) {
if ((err as Error).message === 'ACCOUNT_LOCKED') {
return res.status(429).json({
error: 'Account temporaneamente bloccato. Contatta il supporto o riprova tra 30 minuti.',
});
}
console.error('Login error:', err);
res.status(500).json({ error: 'Errore interno del server' });
}
});
Angular: ochrona, przechwytywacz i bezpieczeństwo po stronie klienta
Kluczową różnicą, gdy wdrażasz uwierzytelnianie w aplikacji Angular, jest: ja strażnik trasy chroni nawigację w interfejsie użytkownika, a nie bezpieczeństwo danych. Osoba atakująca zawsze może ominąć Angulara, modyfikując adres URL lub wywołując interfejsy API bezpośrednio z narzędzi takich jak curl lub Postman. Prawdziwe bezpieczeństwo zawsze kryje się tylko w backendzie.
Strażnicy tras nie są bezpieczeństwem
I CanActivate Osłony kątowe chronią nawigację w interfejsie użytkownika, ale nie zapobiegają
bezpośredni dostęp do API. Każdy punkt końcowy interfejsu API musi zostać odpowiednio uwierzytelniony i autoryzowany
całkowicie niezależny od frontendu. Strażnicy służą wyłącznie do pokazywania strony
zaloguj się do użytkownika zamiast do dashboardu, a nie po to, aby chronić dane.
// auth.guard.ts - Guard per navigazione UI (non sicurezza API)
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
import { map, take } from 'rxjs/operators';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isAuthenticated$.pipe(
take(1),
map((isAuthenticated) => {
if (isAuthenticated) {
return true;
}
// Salva URL per redirect post-login
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
})
);
};
// auth.interceptor.ts - CSRF token e refresh automatico
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
// Aggiunge CSRF token per richieste mutative (POST/PUT/PATCH/DELETE)
const csrfToken = getCookie('XSRF-TOKEN');
const modifiedReq = csrfToken && isStatefulMethod(req.method)
? req.clone({
headers: req.headers.set('X-XSRF-TOKEN', csrfToken),
withCredentials: true, // Invia cookie di sessione con le richieste
})
: req.clone({ withCredentials: true });
return next(modifiedReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Prova refresh automatico del token
return auth.refreshToken().pipe(
switchMap(() => next(modifiedReq)),
catchError((refreshError) => {
// Refresh fallito: forza logout e redirect al login
auth.logout();
return throwError(() => refreshError);
})
);
}
return throwError(() => error);
})
);
};
function isStatefulMethod(method: string): boolean {
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase());
}
function getCookie(name: string): string | null {
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
return match ? decodeURIComponent(match[2]) : null;
}
Lista kontrolna OWASP A07: Błędy identyfikacji i uwierzytelnienia
OWASP A07:2021 (Błędy identyfikacji i uwierzytelnienia) przesuwa się z czwartej na siódmą pozycję w porównaniu do 2017 r., dzięki szerszemu zastosowaniu MFA i menedżerów haseł. Jednak pozostaje krytyczny: Poniższa lista kontrolna podsumowuje minimalne kontrole zapewniające zgodność z najlepszymi praktykami OWASP 2025. Pamiętaj: 45% kodu generowanego przez sztuczną inteligencję nie przechodzi testów bezpieczeństwa na uwierzytelnieniu, Zawsze testuj kod wygenerowany przez narzędzia takie jak GitHub Copilot lub ChatGPT.
Lista kontrolna bezpiecznego uwierzytelniania (OWASP 2025)
- Hashowanie hasła: Argon2id lub bcrypt (współczynnik kosztu >= 12), nigdy proste MD5/SHA
- Polityka haseł: Minimalna długość 8, maksymalnie co najmniej 64 znaki, porównanie HaveIBeenPwned
- Ustalanie sesji: Wygeneruj ponownie identyfikator sesji po każdym logowaniu za pomocą
req.session.regenerate() - Flagi plików cookie: HttpOnly + Secure + SameSite we wszystkich plikach cookie sesji i uwierzytelniania
- Wyloguj się: Zniszcz sesję po stronie serwera, a nie tylko usuń plik cookie
- Limit czasu sesji: Wygaśnięcie braku aktywności (30-60 min dla wrażliwych danych)
- Algorytm JWT: Określ jawną białą listę, blokuj „brak” i nieoczekiwane algorytmy
- Pamięć JWT: Plik cookie HttpOnly, nigdy localStorage ani sessionStorage
- Brutalna siła: Ograniczenie szybkości dla IP + blokada konta z powiadomieniem użytkownika
- Wyliczenie użytkowników: Ogólna odpowiedź na złe dane uwierzytelniające, porównanie bezpieczne pod względem czasowym
- Ministerstwo Spraw Zagranicznych: TOTP dostępny dla wszystkich, obowiązkowy dla ról administratora/uprzywilejowanych
- OAuth 2.1: Wymagane PKCE, brak ukrytego przepływu, brak przyznawania hasła
- Klucze: Rozważ tę opcję jako bezhasłową alternatywę dla nowych projektów
- Dzienniki audytu: Rejestruj wszystkie zdarzenia uwierzytelniania za pomocą adresu IP, agenta użytkownika i znacznika czasu
- Osłony kątowe: Tylko dla UX, prawdziwe bezpieczeństwo zawsze i tylko w backendzie
- CSRF: Token CSRF dla SPA, które korzystają z plików cookie sesji między źródłami
Wnioski
Bezpieczne uwierzytelnianie w 2025 r. wymaga wielowarstwowej strategii: silnego hashowania haseł Argon2id, prawidłowe zarządzanie sesją za pomocą plików cookie HttpOnly+Secure+SameSite, ograniczanie szybkości i konta blokada brute-force, MFA z TOTP dla wszystkich uprzywilejowanych użytkowników i OAuth 2.1 z PKCE dla uwierzytelniania federacyjnego. Każda warstwa łagodzi określoną kategorię ataków; też to pomiń tylko jeden otwiera okna zawierające prawdziwe luki w zabezpieczeniach.
Najważniejszą zmianą na najbliższe lata będzie przejście na klucze: Z założenia odporna na phishing, bez haseł do zapamiętania i hashowania w bazie danych, z UX lepszy dzięki Face ID i Touch ID. Jeśli budujesz nową aplikację w 2025 roku, zastanów się poważnie klucze jako pierwszy czynnik uwierzytelniający.
Na koniec krytyczna uwaga na temat kodu generowanego przez sztuczną inteligencję: 45% kodu uwierzytelniającego jest generowane przez Narzędzia AI nie przechodzą testów bezpieczeństwa. Najczęstszymi problemami są sesje bez regeneracji po login, JWT przechowywany w localStorage, brak bezpiecznego porównania czasowego i pliki cookie bez prawidłowe flagi bezpieczeństwa. Zawsze używaj tej listy kontrolnej OWASP jako ostatecznej recenzji kodu, niezależnie od tego, kto (lub co) to napisał.
Kontynuuj serię dotyczącą bezpieczeństwa sieciowego
- Poprzedni artykuł: Wstrzykiwanie SQL i sprawdzanie poprawności danych wejściowych: bezpieczeństwo zaplecza
- Następny artykuł: Bezpieczeństwo API: OAuth 2.1, JWT i ograniczanie szybkości
- Związane z bezpieczeństwem: XSS, CSRF i CSP: Bezpieczeństwo frontonu
- Powiązane szyfrowanie: Błędy kryptograficzne: haszowanie, szyfrowanie i token
- Powiązane serie: Frontend DevOps: potok DevSecOps







