05 - Bezpieczeństwo API: OAuth 2.1, JWT i ograniczanie szybkości
Interfejsy API stanowią podstawę nowoczesnych aplikacji. Każdy mikroserwis, każda aplikacja mobilna, każda integracja SaaS przechodzi przez punkty końcowe HTTP, które udostępniają krytyczne dane i funkcje. Według raportu Salt Security Stan bezpieczeństwa API 2024, liczba ataków API wzrosła o 167% w ciągu ostatnich 12 miesięcy, przy czym 94% organizacji doświadczyło co najmniej jednego takiego zdarzenia Incydent bezpieczeństwa związany z interfejsem API w ciągu roku. Nie są one dalekie od statystyk codzienna rzeczywistość: zdecydowana większość tych luk dotyczy wzorców stosowanych przez programistów powtarzają się każdego dnia.
Problem ma charakter strukturalny. Interfejsy API są projektowane z myślą o funkcjonalności i bezpieczeństwie dodana później jako warstwa wierzchnia. Dodajesz token JWT i rozważasz skończoną pracę. Jednak ranking OWASP API Security Top 10:2023 pokazuje, że najważniejsze wektory nie mają charakteru technicznego: są logiczne. Autoryzacja na poziomie uszkodzonego obiektu, Autoryzacja na poziomie uszkodzonej funkcji, bez ograniczeń Zużycie zasobów. Luki, których żaden framework nie rozwiązuje automatycznie, bo tego wymagają przemyślane decyzje architektoniczne.
W tym artykule omówiono całą powierzchnię ataku nowoczesnych interfejsów API: począwszy od interfejsu API OWASP Najlepsze ograniczenia szybkości 10:2023 za pomocą algorytmów zbioru tokenów i przesuwanego okna, od OAuth 2.1 z zakresami szczegółowa weryfikacja danych wejściowych, od prawidłowej konfiguracji CORS po wzorce bram API. Praktyczny kod Node.js/Express dla każdej sekcji z ostateczną listą kontrolną specyficzną dla Angulara.
Czego się nauczysz
- OWASP API Security Top 10:2023: 10 najbardziej krytycznych luk z praktycznymi przykładami
- Ograniczanie szybkości: implementacja zasobnika tokenów i przesuwanego okna za pomocą Redis w Node.js
- OAuth 2.1 z zakresami szczegółowymi: różnice w stosunku do OAuth 2.0 i obowiązkowego PKCE
- Najlepsze praktyki JWT: bezpieczne algorytmy, rotacja tokenów, unieważnianie i czarna lista
- Klucze API a tokeny na okaziciela: kiedy zastosować jakie podejście i jak bezpiecznie nimi zarządzać
- Walidacja i oczyszczanie danych wejściowych za pomocą Zoda dla TypeScript
- Bezpieczna konfiguracja CORS: biała lista pochodzenia i zarządzanie danymi uwierzytelniającymi
- Wzorce bram API: scentralizowane uwierzytelnianie, wyłącznik automatyczny, bezpieczne logowanie
- Monitorowanie i ostrzeganie: wykrywaj wzorce ataków w czasie rzeczywistym
- Angularowa lista kontrolna dla bezpiecznych wywołań API z frontendu
Bezpieczeństwo API OWASP Top 10:2023
OWASP API Security Top 10:2023 i lista referencyjna pozwalająca zrozumieć najbardziej krytyczne ryzyka w nowoczesnych API. W porównaniu z wersją 2019 wprowadza trzy nowe kategorie i porządkuje priorytety w oparciu o rzeczywiste dane dotyczące wypadków. Nie jest to lista teoretyczna: każdy wpis odpowiada podatności udokumentowane, które w ostatnich latach spowodowały rzeczywiste naruszenia.
API1:2023 — Autoryzacja na poziomie uszkodzonego obiektu (BOLA)
BOLA była numerem jeden przez trzy kolejne edycje i stanowiła około 40% wszystkich ataków
Udokumentowane interfejsy API. API zwraca zasoby identyfikowane poprzez identyfikator bez weryfikacji użytkownika
uwierzytelniony ma prawa do tego konkretnego zasobu. Oraz wersja API klasycznego IDOR-a
(Niebezpieczne bezpośrednie odniesienie do obiektu): Klient pyta /api/invoices/1234 i serwer
odpowiada, nie sprawdzając, czy faktura 1234 należy do użytkownika, który o nią prosi.
// VULNERABILE: L'utente può leggere le fatture di qualsiasi cliente
app.get('/api/invoices/:id', authenticate, async (req, res) => {
// Manca la verifica che req.user.id == invoice.customerId
const invoice = await Invoice.findById(req.params.id);
res.json(invoice); // Restituisce qualsiasi fattura, a chiunque sia autenticato
});
// CORRETTO: Verifica sempre che la risorsa appartenga all'utente
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await Invoice.findOne({
_id: req.params.id,
customerId: req.user.id, // Filtra per proprietario nel query
});
if (!invoice) {
// Restituisci 404, non 403 (non rivelare l'esistenza della risorsa)
return res.status(404).json({ error: 'Invoice not found' });
}
res.json(invoice);
});
// Con TypeORM: verifica BOLA con query builder
const invoice = await invoiceRepository.findOne({
where: {
id: parseInt(req.params.id),
customer: { id: req.user.id }, // JOIN implicito con verifica ownership
},
});
// Pattern helper per verifiche BOLA sistematiche
async function findOwnedResource<T>(
model: Model<T>,
resourceId: string,
ownerId: string,
ownerField = 'userId'
): Promise<T | null> {
return model.findOne({
_id: resourceId,
[ownerField]: ownerId,
});
}
API2:2023 — Zerwane uwierzytelnianie
Zepsute uwierzytelnienie to nie tylko brak loginu. Zawiera token JWT z algorytmem
none, słabe klucze podpisu, brak weryfikacji odbiorców (aud) e
emitenta (iss), tokeny, które nigdy nie wygasają i brak ochrony przed brutalną siłą
na punktach końcowych logowania. Osoba atakująca, która znajdzie podpis JWT za pomocą HS256 i jeden
słaby klucz np secret każdą rolę można ponownie przypisać.
API3:2023 — Autoryzacja na poziomie właściwości uszkodzonego obiektu (BOPLA)
Nowość w 2023 r. BOPLA łączy w sobie dwie poprzednie luki: Nadmierne ujawnienie danych
(API zwraca więcej pól niż to konieczne, w tym dane wrażliwe) e Przydział masowy
(interfejs API akceptuje pola, których nie powinien, umożliwiając użytkownikowi edycję isAdmin
o role). Bezpieczny wzór i jawna projekcja pól zarówno na wejściu, jak i na wyjściu.
// VULNERABILE: Mass Assignment - l'utente può settare isAdmin
app.put('/api/users/:id', authenticate, async (req, res) => {
// req.body può contenere { name: 'John', isAdmin: true, role: 'superadmin' }
await User.findByIdAndUpdate(req.params.id, req.body); // Accetta tutto
});
// CORRETTO: Whitelist esplicita dei campi modificabili con Zod
import { z } from 'zod';
const UpdateUserSchema = z.object({
name: z.string().min(2).max(100).optional(),
email: z.string().email().optional(),
bio: z.string().max(500).optional(),
// isAdmin, role, permissions: NON presenti nello schema = non accettati
});
app.put('/api/users/:id', authenticate, async (req, res) => {
const parsed = UpdateUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
// Solo i campi validati vengono aggiornati
await User.findByIdAndUpdate(req.params.id, parsed.data);
res.json({ success: true });
});
// VULNERABILE: Excessive Data Exposure
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // Restituisce passwordHash, twoFactorSecret, internalNotes...
});
// CORRETTO: Proiezione esplicita - solo campi pubblici
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id)
.select('name email avatar bio createdAt -_id');
res.json(user);
});
API4:2023 – Nieograniczone zużycie zasobów
Zastępuje „Brak zasobów i ograniczenie szybkości”, ta kategoria obejmuje wszystkie scenariusze gdzie API nie ogranicza zużycia zasobów: żądania bez paginacji, które zwracają miliony rekordów, przesyłanie plików bez ograniczeń rozmiaru, zapytania GraphQL z nieograniczoną głębokością, webhooki bez przekroczeń limitów czasu i operacji intensywnie obciążających procesor bez ograniczania przepustowości. Samo ograniczenie szybkości nie wystarczy: potrzebujesz ograniczeń na każdym poziomie potoku.
API5 - API10: Inne krytyczne luki
- API5 - Uszkodzona autoryzacja poziomu funkcji: Administracyjne punkty końcowe dostępne dla zwykłych użytkowników (często ukryte w dokumentacji, ale nie chronione kodem)
- API6 – Nieograniczony dostęp do wrażliwych przepływów biznesowych: Nadużywanie legalnych przepływów, takich jak masowe tworzenie kont, automatyczne zakupy lub masowe wysyłanie OTP
- API7 — fałszowanie żądań po stronie serwera (SSRF): API akceptuje adresy URL dostarczone przez klienta i żąda ich po stronie serwera, umożliwiając dostęp do usług wewnętrznych
- API8 — błędna konfiguracja zabezpieczeń: Brakujące nagłówki, symbol wieloznaczny CORS, tryb debugowania w środowisku produkcyjnym, przestarzałe wersje API ujawnione bez uwierzytelnienia
- API9 – Niewłaściwe zarządzanie zapasami: Przestarzałe wersje interfejsu API nie zostały usunięte, punkty końcowe testowane w środowisku produkcyjnym, interfejsy API w tle nieudokumentowane
- API10 — Niebezpieczne korzystanie z interfejsów API: Ślepe zaufanie do interfejsów API innych firm bez sprawdzania wyników i obsługi błędów
Ograniczanie szybkości: algorytmy i implementacja
Ograniczanie szybkości to mechanizm zapobiegający nadużyciom API poprzez ograniczenie liczby żądań które klient może wykonać w określonym czasie. To nie tylko środek przeciw DDoS: on chroni z upychania poświadczeń, skrobania, wyliczania zasobów i nadużyć w przepływie biznesowym. Wybór algorytmu wpływa na wygodę użytkownika i skuteczną ochronę.
Algorytm wiadra tokenów
Wiadro tokenów jest najpopularniejszym algorytmem ograniczania szybkości. Każdy klient ma „wiadro” z maksymalna pojemność N żetonów. Tokeny dodawane są ze stałą szybkością R tokenów na sekundę. Każde żądanie zużywa token. Jeśli zasobnik jest pusty, żądanie zostanie odrzucone za pomocą protokołu HTTP 429. Zaletą jest to, że pozwala wzrost ruchu do pojemności wiadra, następnie stabilizuje się na poziomie R żądań na sekundę. Idealny dla publicznych interfejsów API z ruchem fal naturalnych.
// Token Bucket con Redis - implementazione production-ready
// npm install ioredis
import Redis from 'ioredis';
import { Request, Response, NextFunction } from 'express';
const redis = new Redis(process.env.REDIS_URL!);
interface TokenBucketConfig {
capacity: number; // capacità massima del secchio (burst max)
refillRate: number; // Token aggiunti per secondo (regime stazionario)
}
class TokenBucketLimiter {
constructor(private config: TokenBucketConfig) {}
async checkLimit(key: string): Promise<{
allowed: boolean;
remaining: number;
resetMs: number;
}> {
const now = Date.now();
const bucketKey = `rl:tb:${key}`;
// Lua script per operazione atomica - critico per evitare race condition
const luaScript = `
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1]) or capacity
local last_ts = tonumber(data[2]) or now
-- Calcola token da aggiungere in base al tempo trascorso
local elapsed_sec = (now - last_ts) / 1000.0
local refilled = math.min(capacity, tokens + elapsed_sec * refill_rate)
if refilled >= 1.0 then
local remaining = refilled - 1.0
redis.call('HMSET', KEYS[1], 'tokens', remaining, 'ts', now)
redis.call('PEXPIRE', KEYS[1], 3600000)
return {1, math.floor(remaining)}
else
redis.call('HMSET', KEYS[1], 'tokens', refilled, 'ts', now)
redis.call('PEXPIRE', KEYS[1], 3600000)
return {0, 0}
end
`;
const result = await redis.eval(
luaScript, 1, bucketKey,
this.config.capacity,
this.config.refillRate,
now
) as [number, number];
const resetMs = result[0] === 0
? Math.ceil((1 / this.config.refillRate) * 1000)
: 0;
return {
allowed: result[0] === 1,
remaining: result[1],
resetMs,
};
}
middleware() {
return async (req: Request, res: Response, next: NextFunction) => {
// Chiave per utente autenticato, per IP altrimenti
const key = (req as any).user?.id
? `user:${(req as any).user.id}`
: `ip:${req.ip}`;
const { allowed, remaining, resetMs } = await this.checkLimit(key);
// Headers standard RateLimit (RFC 6585 + draft-ietf-httpapi-ratelimit-headers)
res.setHeader('X-RateLimit-Limit', this.config.capacity);
res.setHeader('X-RateLimit-Remaining', remaining);
res.setHeader('X-RateLimit-Reset', Date.now() + resetMs);
res.setHeader('Retry-After', Math.ceil(resetMs / 1000));
if (!allowed) {
return res.status(429).json({
error: 'Too Many Requests',
message: `Rate limit exceeded. Retry after ${Math.ceil(resetMs / 1000)}s`,
retryAfter: Math.ceil(resetMs / 1000),
});
}
next();
};
}
}
// Limiter differenziati per endpoint con profili diversi
export const apiLimiter = new TokenBucketLimiter({
capacity: 100, // 100 richieste burst
refillRate: 10, // 10 req/s regime stazionario
});
export const authLimiter = new TokenBucketLimiter({
capacity: 5, // Solo 5 tentativi consecutivi (anti brute-force)
refillRate: 0.017, // ~1 token/minuto
});
export const uploadLimiter = new TokenBucketLimiter({
capacity: 10,
refillRate: 0.1, // 1 upload ogni 10 secondi
});
Algorytm przesuwanego okna
W stałym oknie występuje znany problem: osoba atakująca może skoncentrować N żądań na końcu jednego okna i N żądań na początku następnego, co daje 2N żądań w krótkim czasie czasu bez naruszania limitu technicznego. Przesuwane okno rozwiązuje ten problem, licząc dokładnie dla każdego okna czasowego, które „płynie” w czasie, używając zestawu sortowanego Redis, gdzie każdy element ma znacznik czasu żądania jako swój wynik.
// Sliding Window Counter con Redis Sorted Set
class SlidingWindowLimiter {
constructor(
private limit: number, // Numero massimo richieste nella finestra
private windowMs: number // Dimensione finestra in millisecondi
) {}
async isAllowed(identifier: string): Promise<{
allowed: boolean;
count: number;
resetMs: number;
}> {
const now = Date.now();
const windowStart = now - this.windowMs;
const key = `rl:sw:${identifier}`;
// Pipeline Redis per operazioni atomiche in sequenza
const pipeline = redis.pipeline();
// 1. Rimuovi elementi fuori dalla finestra temporale
pipeline.zremrangebyscore(key, '-inf', windowStart);
// 2. Aggiungi la richiesta corrente (member unico con timestamp + random)
pipeline.zadd(key, now, `${now}:${Math.random().toString(36).slice(2)}`);
// 3. Conta le richieste nella finestra corrente
pipeline.zcard(key);
// 4. TTL automatico per non accumulare chiavi orfane
pipeline.pexpire(key, this.windowMs);
const results = await pipeline.exec();
const count = results![2][1] as number;
// Calcola quando la richiesta più vecchia uscira dalla finestra
const oldest = await redis.zrange(key, 0, 0, 'WITHSCORES');
const resetMs = oldest.length > 1
? Math.max(0, parseInt(oldest[1]) + this.windowMs - now)
: this.windowMs;
return {
allowed: count <= this.limit,
count,
resetMs,
};
}
}
// Factory per middleware Express con key generator personalizzabile
function slidingWindowMiddleware(
limit: number,
windowMs: number,
keyGen?: (req: Request) => string
) {
const limiter = new SlidingWindowLimiter(limit, windowMs);
return async (req: Request, res: Response, next: NextFunction) => {
const key = keyGen
? keyGen(req)
: ((req as any).user?.id ?? req.ip ?? 'anon');
const { allowed, count, resetMs } = await limiter.isAllowed(key);
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - count));
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + resetMs).toISOString());
if (!allowed) {
return res.status(429).json({
error: 'Too Many Requests',
retryAfter: Math.ceil(resetMs / 1000),
});
}
next();
};
}
// Utilizzo: rate limit globale + specifici per endpoint sensibili
app.use('/api/', slidingWindowMiddleware(1000, 60_000)); // 1000/min globale
app.use('/api/auth/', slidingWindowMiddleware(10, 15 * 60_000)); // 10/15min per auth
app.post(
'/api/payments',
slidingWindowMiddleware(
5, 60 * 60_000,
(req) => `pay:${(req as any).user!.id}` // Per-user invece che per-IP
)
);
Wiadro tokenów a przesuwane okno: przewodnik po wyborze
| Kryterium | Wiadra na żetony | Przesuwane okno |
|---|---|---|
| Wstrząs ruchu | Umożliwia serie do pojemności łyżki | Zastosuj jednolity limit, bez serii |
| Ogranicz dokładność | Przybliżona (zależy od szybkości uzupełniania) | Z dokładnością do milisekundy |
| Magazyn Redis | 2 wartości na klucz (lekki skrót) | N elementów na klucz (zestaw posortowany, proporcjonalny do ruchu) |
| Szczytowe zachowanie | Pochłania naturalne szczyty bez błędów | Trudne: przekroczenie limitu zwraca 429 |
| Idealny przypadek użycia | Publiczne API, CDN, zmienny ruch użytkowników | Krytyczne punkty końcowe: autoryzacja, płatności, OTP |
OAuth 2.1 i JWT: nowoczesne uwierzytelnianie API
OAuth 2.1 (wersja robocza RFC) konsoliduje najlepsze praktyki dotyczące bezpieczeństwa OAuth 2.0. Kluczowe różnice w porównaniu do OAuth 2.0 są to: PKCE obowiązkowe dla wszystkich klientów (nie tylko publiczne klienci), Usunięto ukryty przepływ, poświadczenia hasła właściciela zasobu przepływ usunięty, I dokładne dopasowanie URI przekierowania obowiązkowe (nic dopasowanie wzorca za pomocą symbolu wieloznacznego). Jeśli Twoja implementacja OAuth 2.0 była już zgodna z OWASP Security Ściągawka. Przejście na OAuth 2.1 wymaga minimalnych zmian.
Zakresy szczegółowe: minimalne uprawnienia dla interfejsów API
Zakresy OAuth definiują, co może zrobić token. Zakresy zbyt szerokie read:all
o admin naruszają zasadę najmniejszych przywilejów. Skradziony token z lunetą
invoices:read:own powoduje znacznie bardziej ograniczone obrażenia niż jeden z
read:all. Szczegółowość zakresów musi odzwierciedlać rzeczywiste operacje interfejsu API.
// Definizione scopes granulari per API di fatturazione
const SCOPES = {
// Formato: risorsa:operazione:contesto
'invoices:read:own': 'Leggere le proprie fatture',
'invoices:read:all': 'Leggere tutte le fatture (solo admin)',
'invoices:create': 'Creare nuove fatture',
'invoices:update:own': 'Modificare le proprie fatture',
'invoices:delete:own': 'Eliminare le proprie fatture',
'customers:read:own': 'Leggere il proprio profilo cliente',
'customers:read:all': 'Leggere tutti i clienti (solo admin)',
'reports:read:financial': 'Accedere ai report finanziari',
'payments:create': 'Effettuare pagamenti',
'webhooks:manage': 'Gestire i webhook',
} as const;
type Scope = keyof typeof SCOPES;
// Middleware scope check
function requireScope(...requiredScopes: Scope[]) {
return (req: Request, res: Response, next: NextFunction) => {
const tokenScopes: string[] = ((req as any).user?.scope ?? '').split(' ');
const hasScope = requiredScopes.every(s => tokenScopes.includes(s));
if (!hasScope) {
return res.status(403).json({
error: 'insufficient_scope',
required: requiredScopes,
granted: tokenScopes,
});
}
next();
};
}
// Route con scope checking granulare
app.get('/api/invoices',
authenticate,
requireScope('invoices:read:own'),
async (req, res) => {
const invoices = await Invoice.find({ userId: (req as any).user.id });
res.json(invoices);
}
);
app.get('/api/admin/invoices',
authenticate,
requireScope('invoices:read:all'), // Scope admin separato
async (req, res) => {
const invoices = await Invoice.find({});
res.json(invoices);
}
);
// Scope multipli richiesti contemporaneamente
app.post('/api/reports/financial',
authenticate,
requireScope('reports:read:financial', 'invoices:read:all'),
async (req, res) => {
// Richiede ENTRAMBI gli scopes
}
);
JWT: 6 fatalnych błędów
JWT jest szeroko stosowany, ale równie powszechnie słabo wdrażany. Te sześć błędnych wzorców zamieniają solidny mechanizm uwierzytelniania w krytyczną lukę. Każdy punkt odpowiada udokumentowanemu CVE lub rzeczywistemu schematowi ataku.
// ERRORE 1: Accettare l'algoritmo 'none'
// CVE-2015-9235 - Molte librerie JWT pre-2016 lo accettavano
// Un attaccante modifica header.alg = "none" e rimuove la firma
// VULNERABILE:
const decoded = jwt.verify(token, secret); // Default: accetta tutti gli algoritmi
// CORRETTO: Whitelist esplicita degli algoritmi
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Solo asimmetrico RS256 per sistemi distribuiti
// oppure ['HS256'] con chiave forte (min 256 bit) per sistemi single-server
});
// ERRORE 2: Chiave simmetrica debole
// VULNERABILE: Brute-forceable in pochi secondi
const token = jwt.sign(payload, 'secret');
const token2 = jwt.sign(payload, 'mysecretkey123');
// CORRETTO: Usa RS256 (asimmetrico) oppure HS256 con chiave forte
// Genera chiave RS256:
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -pubout -out public.pem
import fs from 'fs';
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
// ERRORE 3: Nessuna validazione claims iss/aud
// Un token emesso per un altro servizio potrebbe essere accettato
// CORRETTO: Verifica issuer e audience
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'https://auth.myapp.com', // Chi ha emesso il token
audience: 'https://api.myapp.com', // Per chi e il token
// exp verificato automaticamente dalla libreria
});
// ERRORE 4: Access token con durata eccessiva
// VULNERABILE: Token valido 30 giorni = finestra di attacco enorme
const token = jwt.sign(payload, secret, { expiresIn: '30d' });
// CORRETTO: Access token breve (15min) + refresh token
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
scope: user.scopes.join(' '),
jti: crypto.randomUUID(), // JWT ID univoco per blacklisting
},
privateKey,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
}
);
// Refresh token: opaco, salvato nel DB, revocabile
const refreshToken = crypto.randomBytes(32).toString('base64url');
await RefreshToken.create({
token: await bcrypt.hash(refreshToken, 12), // Hash nel DB, mai in chiaro
userId: user.id,
jti: crypto.randomUUID(),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
userAgent: req.get('User-Agent') ?? '',
ipAddress: req.ip ?? '',
});
// ERRORE 5: Refresh token senza rotazione
// Rotation: ogni uso genera un nuovo refresh token, il vecchio viene invalidato
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
// Cerca token attivi per l'utente
const stored = await RefreshToken.findOne({
userId: (req as any).session?.userId,
used: false,
expiresAt: { $gt: new Date() },
});
if (!stored || !(await bcrypt.compare(refreshToken, stored.token))) {
// Token non valido o già usato: REVOCA TUTTO (possibile furto)
await RefreshToken.deleteMany({ userId: stored?.userId });
return res.status(401).json({ error: 'Invalid refresh token' });
}
await stored.updateOne({ used: true }); // Invalida il vecchio
// Emetti nuovi token
const newAccessToken = generateAccessToken(stored.userId);
const newRefreshToken = crypto.randomBytes(32).toString('base64url');
await RefreshToken.create({
token: await bcrypt.hash(newRefreshToken, 12),
userId: stored.userId,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});
// Refresh token via HttpOnly cookie (non nel body)
res.cookie('rt', newRefreshToken, {
httpOnly: true, secure: true, sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000,
path: '/api/auth/refresh',
});
res.json({ accessToken: newAccessToken });
});
// ERRORE 6: JWT in localStorage
// localStorage e accessibile da qualsiasi script JS sulla pagina (XSS)
// CORRETTO: access token in-memory, refresh token in HttpOnly cookie
// Vedi sezione Angular checklist per l'implementazione lato client
Klucze API: bezpieczne zarządzanie integracją serwer-serwer
Klucze API są właściwym wyborem w przypadku komunikacji maszyna-maszyna tam, gdzie znajduje się klient kontrolowane przez właściciela konta (skrypty automatyzujące, backendy innych firm, integracje CI/CD). Są prostsze niż OAuth, ale wymagają ostrożnego zarządzania: kluczem API należy je traktować jak hasło i nigdy nie ujawniać go w logach ani kodzie źródłowym.
// Gestione sicura API Keys - pattern ispirato a Stripe
import crypto from 'crypto';
import { timingSafeEqual } from 'crypto';
// Formato con prefisso identificativo del tipo di chiave
// sk_live_xxxx = production, sk_test_xxxx = sandbox
function generateApiKey(env: 'live' | 'test' = 'live'): {
key: string;
hash: string;
prefix: string;
} {
const random = crypto.randomBytes(24).toString('base64url');
const key = `sk_${env}_${random}`;
const prefix = key.slice(0, 10) + '...'; // Per display nell'UI
// SHA-256 del token: se il DB viene compromesso, le chiavi sono al sicuro
const hash = crypto.createHash('sha256').update(key).digest('hex');
return { key, hash, prefix };
}
// Autenticazione via API key con timing-safe comparison
async function authenticateApiKey(req: Request, res: Response, next: NextFunction) {
const rawKey =
req.headers['x-api-key'] as string ??
req.headers.authorization?.replace('Bearer ', '');
if (!rawKey) {
return res.status(401).json({ error: 'API key required' });
}
const incomingHash = crypto.createHash('sha256').update(rawKey).digest('hex');
// Cerca per hash nel DB
const storedKey = await ApiKey.findOne({
hash: incomingHash,
active: true,
expiresAt: { $gt: new Date() },
}).populate('owner');
if (!storedKey) {
// Timing-safe: evita timing attack per dedurre chiavi valide
const fakeBuf = Buffer.alloc(32, 0);
timingSafeEqual(
Buffer.from(incomingHash, 'hex'),
fakeBuf
);
return res.status(401).json({ error: 'Invalid or expired API key' });
}
// Aggiorna metadati per auditing
await ApiKey.findByIdAndUpdate(storedKey._id, {
lastUsedAt: new Date(),
$inc: { requestCount: 1 },
lastUsedIp: req.ip,
});
(req as any).user = storedKey.owner;
(req as any).apiKey = storedKey; // Dati chiave per rate limiting specifico
next();
}
// Endpoint creazione chiave con scopes e rate limit personalizzato
app.post('/api/keys', authenticate, async (req, res) => {
const schema = z.object({
name: z.string().min(1).max(100),
scopes: z.array(z.string()).min(1),
expiresInDays: z.number().int().min(1).max(365).default(90),
rateLimit: z.number().int().min(10).max(10000).default(1000),
environment: z.enum(['live', 'test']).default('live'),
});
const parsed = schema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten() });
}
const { key, hash, prefix } = generateApiKey(parsed.data.environment);
await ApiKey.create({
name: parsed.data.name,
hash, // Solo hash nel DB, MAI la chiave in chiaro
prefix, // Per identificazione nell'UI senza esporre la chiave
ownerId: (req as any).user.id,
scopes: parsed.data.scopes,
rateLimit: parsed.data.rateLimit,
expiresAt: new Date(Date.now() + parsed.data.expiresInDays * 86_400_000),
});
// Restituisce la chiave UNA SOLA VOLTA (pattern Stripe/GitHub)
res.status(201).json({
key, // Solo in questo momento, non più recuperabile
prefix, // Per display futuro
message: 'Store this key securely. It will not be shown again.',
});
});
Walidacja danych wejściowych za pomocą Zoda
Walidacja danych wejściowych jest pierwszą linią obrony przed wtryskiem, przypisaniem masy i nieoczekiwane zachowania. W przypadku interfejsu API TypeScript, Zoda oraz narzędzie referencyjne w 2025 r.: zdefiniuj schemat raz i uzyskaj weryfikację środowiska wykonawczego oraz wywnioskowane typy TypeScript automatycznie, bez duplikacji między interfejsami TypeScript i walidatorami środowiska wykonawczego. Każdy punkt końcowy musi osobno sprawdzić treść, parametry i ciąg zapytania.
// Input validation completa con Zod
// npm install zod
import { z } from 'zod';
// Schema con regole di sicurezza specifiche
const CreateOrderSchema = z.object({
// UUID format: previene injection via ID malformati
customerId: z.string().uuid('ID cliente non valido'),
// Limiti di lunghezza espliciti: previene DoS via stringhe enormi
notes: z.string().max(1000).optional().transform(v => v?.trim()),
// Array con limiti: previene mass insertion
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100), // Limiti di business
unitPrice: z.number().positive().max(99_999.99),
})).min(1).max(50),
// Enum: solo valori predefiniti, no stringhe arbitrarie
shippingMethod: z.enum(['standard', 'express', 'overnight']),
// Data con validazione semantica
requestedDelivery: z.string().datetime().transform(v => new Date(v))
.refine(d => d > new Date(), 'La data deve essere nel futuro')
.optional(),
// URL con vincolo HTTPS
webhookUrl: z.string().url()
.refine(u => u.startsWith('https://'), 'URL deve usare HTTPS')
.optional(),
});
type CreateOrderInput = z.infer<typeof CreateOrderSchema>; // Tipo inferito automaticamente
// Middleware di validazione riutilizzabile
function validateBody<T extends z.ZodType>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
fieldErrors: result.error.flatten().fieldErrors,
});
}
req.body = result.data; // Sostituisci con dati validati e trasformati
next();
};
}
function validateQuery<T extends z.ZodType>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.query);
if (!result.success) {
return res.status(400).json({ error: 'Invalid query parameters' });
}
(req as any).validatedQuery = result.data;
next();
};
}
// Schema per paginazione sicura
const PaginationSchema = z.object({
page: z.coerce.number().int().min(1).max(10_000).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
order: z.enum(['asc', 'desc']).default('desc'),
// Ricerca testuale con sanitizzazione
q: z.string().max(100).optional().transform(v => v?.replace(/[^\w\s-]/g, '')),
});
// Utilizzo nella route
app.post(
'/api/orders',
authenticate,
requireScope('orders:create'),
apiLimiter.middleware(),
validateBody(CreateOrderSchema),
async (req: Request, res: Response) => {
const body = req.body as CreateOrderInput; // Tipizzato e validato
const order = await OrderService.create(body, (req as any).user.id);
res.status(201).json(order);
}
);
Bezpieczna konfiguracja CORS
CORS jest często pierwszą konfiguracją, której programista dotyka podczas wykonywania wywołań API z frontendu
nie rozwijają się. Najczęstsza i błędna odpowiedź: Access-Control-Allow-Origin: *.
To wyłącza ochronę tego samego pochodzenia dla całego interfejsu API. Co gorsza,
Access-Control-Allow-Origin: * con credentials: true to w ogóle nie działa
dla nowoczesnych przeglądarek (ze względów bezpieczeństwa) i powinien natychmiast zgłosić alert podczas przeglądu kodu.
// Configurazione CORS sicura con whitelist dinamica
// npm install cors @types/cors
import cors from 'cors';
// Whitelist separata per ambiente: mai mescolare dev e produzione
const PROD_ORIGINS = [
'https://app.mycompany.com',
'https://admin.mycompany.com',
];
const DEV_ORIGINS = [
'http://localhost:4200', // Angular CLI dev server
'http://localhost:3000',
...PROD_ORIGINS,
];
const allowedOrigins =
process.env.NODE_ENV === 'production' ? PROD_ORIGINS : DEV_ORIGINS;
const corsOptions: cors.CorsOptions = {
origin: (origin, callback) => {
// Permetti richieste senza Origin header (curl, Postman, server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS: origine '${origin}' non autorizzata`));
}
},
credentials: true, // Necessario per cookie cross-origin (refresh token)
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-API-Key',
'X-Correlation-ID',
'X-Requested-With',
],
// Headers che il client può leggere dalla risposta
exposedHeaders: [
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
'X-RateLimit-Reset',
'X-Correlation-ID',
],
maxAge: 86_400, // Cache preflight OPTIONS per 24 ore
};
app.use(cors(corsOptions));
// Gestione esplicita degli errori CORS
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err.message.startsWith('CORS:')) {
return res.status(403).json({
error: 'CORS Policy Violation',
message: 'Origin not allowed',
});
}
next(err);
});
// Security headers con helmet
import helmet from 'helmet';
app.use(helmet({
// Previeni MIME-type sniffing
noSniff: true,
// Rimuovi X-Powered-By (fingerprinting)
hidePoweredBy: true,
// HSTS: forza HTTPS per 1 anno
hsts: {
maxAge: 31_536_000,
includeSubDomains: true,
preload: true,
},
// Limita Referrer nelle richieste cross-origin
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));
Wzorce bram API dla bezpieczeństwa
Brama API centralizuje przekrojowe zagadnienia, takie jak uwierzytelnianie, ograniczanie szybkości i rejestrowanie i przetwarzanie wniosków. W architekturze mikrousług należy unikać replikowania logiki bezpieczeństwo w każdym serwisie. Skład oprogramowania pośredniego Express musi być zgodny z określoną kolejnością: nagłówki bezpieczeństwa przed każdym przetwarzaniem, następnie ograniczanie szybkości, następnie uwierzytelnianie, a następnie walidacja, wreszcie trasy.
// API Gateway middleware stack - ordine critico per la sicurezza
import express from 'express';
import helmet from 'helmet';
import { v4 as uuidv4 } from 'uuid';
const app = express();
// STEP 1: Correlation ID - traccia ogni richiesta end-to-end
app.use((req: Request, res: Response, next: NextFunction) => {
const cid = (req.headers['x-correlation-id'] as string) ?? uuidv4();
(req as any).correlationId = cid;
res.setHeader('X-Correlation-ID', cid);
next();
});
// STEP 2: Security headers
app.use(helmet());
app.use(cors(corsOptions));
// STEP 3: Body parsing con limiti (previene DoS via payload enormi)
app.use(express.json({
limit: '10kb', // 10KB max per JSON - adatta per upload separati
strict: true, // Solo array/oggetti JSON validi
}));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
// STEP 4: Rate limiting globale (prima dell'autenticazione)
app.use('/api/', slidingWindowMiddleware(5000, 60_000)); // 5000 req/min per IP
// STEP 5: Autenticazione (dopo rate limit - non sprecare risorse su token validazione)
app.use('/api/v1/', authenticateRequest);
// STEP 6: Rate limiting per utente autenticato (dopo auth per avere user ID)
app.use('/api/v1/auth/', slidingWindowMiddleware(
10, 15 * 60_000,
(req) => `auth:${req.ip}`
));
// STEP 7: Routes applicative
app.use('/api/v1/', v1Router);
// STEP 8: Error handler sicuro - SEMPRE in fondo
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const correlationId = (req as any).correlationId;
// Log interno dettagliato (solo per team di sviluppo)
console.error({
correlationId,
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
userId: (req as any).user?.id ?? 'anonymous',
});
// Risposta esterna: nessun dettaglio interno in produzione
if (process.env.NODE_ENV === 'production') {
res.status(500).json({
error: 'Internal Server Error',
correlationId, // Per il supporto: consente di trovare il log corrispondente
});
} else {
res.status(500).json({
error: err.message,
correlationId,
});
}
});
// Circuit breaker per servizi upstream - evita cascade failures
import CircuitBreaker from 'opossum';
const paymentBreaker = new CircuitBreaker(
async (payload: unknown) => {
const resp = await fetch('https://payment-svc/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(3000), // 3s timeout
});
if (!resp.ok) throw new Error(`Payment service error: ${resp.status}`);
return resp.json();
},
{
timeout: 3000,
errorThresholdPercentage: 50, // Apre il circuito se 50% delle chiamate fallisce
resetTimeout: 30_000, // Riprova dopo 30s
volumeThreshold: 5, // Minimo 5 chiamate prima di aprire
}
);
paymentBreaker.fallback(() => {
throw new Error('Payment service temporarily unavailable');
});
Monitorowanie: wykrywaj ataki w czasie rzeczywistym
Skuteczny system monitorowania API skupia się na konkretnych wskaźnikach bezpieczeństwa: Poziomy błędów 401/403 na adres IP i na punkt końcowy, wzorce wyliczania identyfikatorów zasobów, nietypowe żądania dotyczące rozmiaru ładunku oraz dostęp do nieudokumentowanych lub przestarzałych punktów końcowych. Ogólne wskaźniki aplikacji nie wystarczą: potrzebne są liczniki zorientowane na bezpieczeństwo.
// Monitoring sicurezza API con metriche orientate agli attacchi
// npm install prom-client
import { Counter, Histogram, register } from 'prom-client';
const authFailureCounter = new Counter({
name: 'api_auth_failures_total',
help: 'Numero totale di fallimenti autenticazione',
labelNames: ['failure_type', 'endpoint'],
});
const rateLimitCounter = new Counter({
name: 'api_rate_limit_hits_total',
help: 'Rate limit superato',
labelNames: ['endpoint', 'identifier_type'],
});
const suspiciousActivityCounter = new Counter({
name: 'api_suspicious_activity_total',
help: 'Attivita potenzialmente sospette rilevate',
labelNames: ['type', 'severity'],
});
// Rilevamento pattern di enumerazione in memoria
// In produzione usa Redis per coordinare più istanze
const enumTracker = new Map<string, { count: number; firstSeen: number }>();
export async function detectEnumeration(
ip: string,
endpoint: string,
statusCode: number
): Promise<void> {
if (statusCode !== 404 && statusCode !== 403) return;
const key = `${ip}:${endpoint.split('/')[2] ?? 'root'}`; // Raggruppa per risorsa
const now = Date.now();
const WINDOW = 60_000; // 1 minuto
const existing = enumTracker.get(key);
if (!existing || now - existing.firstSeen > WINDOW) {
enumTracker.set(key, { count: 1, firstSeen: now });
return;
}
existing.count++;
if (existing.count >= 20) {
suspiciousActivityCounter.inc({
type: 'enumeration_attempt',
severity: existing.count >= 50 ? 'critical' : 'high',
});
if (existing.count === 20) {
// Blocca IP per 1 ora
await redis.setex(`blocked:${ip}`, 3600, '1');
console.warn(`[SECURITY] Enumeration detected from ${ip}, endpoint: ${endpoint}`);
}
}
}
// Middleware raccolta metriche
app.use((req: Request, res: Response, next: NextFunction) => {
res.on('finish', () => {
const endpoint = (req.route?.path ?? req.path).replace(/\/\d+/g, '/:id');
if (res.statusCode === 401) {
authFailureCounter.inc({ failure_type: 'invalid_token', endpoint });
} else if (res.statusCode === 403) {
authFailureCounter.inc({ failure_type: 'forbidden', endpoint });
} else if (res.statusCode === 429) {
const identType = (req as any).user ? 'user' : 'ip';
rateLimitCounter.inc({ endpoint, identifier_type: identType });
}
// Rileva pattern sospetti asincrono (non blocca la risposta)
detectEnumeration(req.ip ?? '', req.path, res.statusCode).catch(console.error);
});
next();
});
// Endpoint metriche (solo per monitoring interno - protetto)
app.get('/internal/metrics', authenticateInternal, async (_, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
Angularowa lista kontrolna dotycząca bezpieczeństwa API
Frontend Angulara ma określone obowiązki w zakresie bezpieczeństwa API. Wybór gdzie przechowywanie tokenów ma bezpośrednie konsekwencje na powierzchni ataku XSS. Przechwytywacz HTTP centralizuje zarządzanie bezpieczeństwem, unikając dystrybucji krytycznej logiki pomiędzy dziesiątkami usług.
// HTTP Interceptor Angular per sicurezza API
// src/app/interceptors/api-security.interceptor.ts
import { inject } from '@angular/core';
import {
HttpInterceptorFn,
HttpRequest,
HttpHandlerFn,
HttpErrorResponse,
} from '@angular/common/http';
import { catchError, switchMap, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const apiSecurityInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
) => {
const auth = inject(AuthService);
const router = inject(Router);
// Aggiungi token solo per le nostre API (non per CDN o API di terze parti)
const isOurApi = req.url.startsWith('/api') ||
req.url.startsWith('https://api.myapp.com');
const accessToken = isOurApi ? auth.getAccessToken() : null;
const secureReq = accessToken
? req.clone({
setHeaders: {
Authorization: `Bearer ${accessToken}`,
'X-Correlation-ID': crypto.randomUUID(),
},
// Invia cookie HttpOnly (refresh token) solo per le nostre API
withCredentials: isOurApi,
})
: req;
return next(secureReq).pipe(
catchError((error: HttpErrorResponse) => {
switch (error.status) {
case 401:
// Token scaduto: tenta refresh automatico una volta
return auth.refreshToken().pipe(
switchMap((newToken: string) =>
next(req.clone({
setHeaders: { Authorization: `Bearer ${newToken}` },
withCredentials: true,
}))
),
catchError(() => {
auth.logout();
router.navigate(['/login'], {
queryParams: { reason: 'session_expired' },
});
return throwError(() => error);
})
);
case 403:
router.navigate(['/forbidden']);
return throwError(() => error);
case 429:
// Non loggare come errore critico: e il rate limiter che funziona
const retryAfter = error.headers.get('Retry-After');
console.warn(`Rate limited. Riprova tra ${retryAfter}s`);
return throwError(() => error);
default:
return throwError(() => error);
}
})
);
};
// AuthService con access token in-memory (resistente a XSS)
import { Injectable, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, tap, map } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
// CRITICO: In-memory, non localStorage/sessionStorage
// Perso al refresh della pagina (feature, non bug: forza re-auth)
private readonly _accessToken = signal<string | null>(null);
readonly isAuthenticated = signal<boolean>(false);
getAccessToken(): string | null {
return this._accessToken();
}
login(email: string, password: string): Observable<void> {
return this.http.post<{ accessToken: string }>(
'/api/auth/login',
{ email, password },
{ withCredentials: true } // Riceve HttpOnly cookie con refresh token
).pipe(
tap(res => {
this._accessToken.set(res.accessToken);
this.isAuthenticated.set(true);
}),
map(() => void 0)
);
}
refreshToken(): Observable<string> {
return this.http.post<{ accessToken: string }>(
'/api/auth/refresh', {},
{ withCredentials: true } // Invia HttpOnly cookie automaticamente
).pipe(
tap(res => this._accessToken.set(res.accessToken)),
map(res => res.accessToken)
);
}
logout(): void {
this._accessToken.set(null);
this.isAuthenticated.set(false);
// Invalida il refresh token lato server
this.http.post('/api/auth/logout', {}, { withCredentials: true })
.subscribe();
}
}
// Registrazione interceptor in app.config.ts
// provideHttpClient(withInterceptors([apiSecurityInterceptor]))
Lista kontrolna bezpieczeństwa API - Angular + Node.js
| Obszar | Sprawdzać | Priorytet |
|---|---|---|
| Autoryzacja obiektu | Kontrola BOLA: każde zapytanie filtruje według właściciela przed zwróceniem danych | KRYTYKA |
| JWT | Algorytm RS256, ważność 15 minut, potwierdzone oświadczenia iss/aud/exp | KRYTYKA |
| Przechowywanie tokenów | Token dostępu w pamięci Angular, odśwież token w pliku cookie HttpOnly SameSite=Strict | KRYTYKA |
| Ograniczanie szybkości | Wiadro tokenów dla ogólnego API, przesuwane okno dla autoryzacji/płatności | WYSOKI |
| Walidacja danych wejściowych | Zod na treści/parametrach/zapytaniu, limity długości, wyliczenie wartości domyślnych | WYSOKI |
| CORS | Jawna biała lista źródeł, bez symboli wieloznacznych z poświadczeniami | WYSOKI |
| Zakresy OAuth | Szczegółowo według zasobu: operacji: kontekstu, sprawdzane w każdym punkcie końcowym | WYSOKI |
| Obsługa błędów | Brak śladu stosu w produkcji, identyfikator korelacji dla wsparcia | PRZECIĘTNY |
| Monitorowanie | Liczniki 401/403/429, wykrywanie wyliczeń, alarmowanie o nieprawidłowościach | PRZECIĘTNY |
| Klucze API | Tylko skrót w bazie danych, nigdy w trybie czystym; porównanie bezpieczne pod względem czasowym; przedrostek identyfikacyjny | PRZECIĘTNY |
Anty-wzorce, których należy bezwzględnie unikać
- JWT w localStorage: Dostępne z dowolnego skryptu JS na stronie. Użyj pliku cookie w pamięci dla tokenu dostępu i pliku cookie HttpOnly dla tokenu odświeżania
- Symbol wieloznaczny CORS z poświadczeniami:
Access-Control-Allow-Origin: *concredentials: truejest z założenia odrzucany przez nowoczesne przeglądarki; oznacza błędną konfigurację - Ograniczanie szybkości tylko dla adresu IP: Botnet rozprowadza ataki na tysiące adresów IP. Połącz ograniczenie szybkości na adres IP z ograniczeniem szybkości na identyfikator użytkownika
- Zakresy są zbyt szerokie:
adminoread:allnaruszają najmniejszy przywilej. Skradziony token z lunetąinvoices:read:ownpowoduje znacznie mniejsze szkody - Ślad stosu ujawniony w produkcji: Ujawnij ścieżki wewnętrzne, wersje bibliotek, strukturę bazy danych. Zawsze używaj procedury obsługi błędów, która zaciemnia szczegóły w produkcji
- Nieokreślony algorytm JWT: Zawsze określaj
algorithms: ['RS256']. Niektóre biblioteki JWT z domyślną konfiguracją akceptują ten algorytmnone - BOLA niezweryfikowana: 40% ataków API wykorzystuje BOLA. Każdy punkt końcowy, który akceptuje identyfikator, musi sprawdzić, czy użytkownik jest właścicielem zasobu
Wnioski
Bezpieczeństwa API nie rozwiązuje się za pomocą jednego narzędzia ani konfiguracji. To praktyka nadal wymaga uwagi na każdym poziomie: od projektu punktu końcowego (sprawdź BOLA systematycznie, zakresy szczegółowe), po realizację ograniczania szybkości (wybór algorytmu prawo do każdego przypadku użycia), po prawidłowe zarządzanie OAuth 2.1 i JWT (RS256, krótkie terminy, odświeżanie rotacji tokenów), aż po monitorowanie w celu wykrycia wzorców ataków, zanim spowodują szkody.
Dane z lat 2024-2025 pokazują, że większość incydentów API nie wiąże się z podatnościami egzotyczne: dotyczy podstawowych błędów uprawnień (nie sprawdzanie, czy zasób należy użytkownikowi, który o to poprosi), zezwalające konfiguracje CORS, tokeny JWT bez ważności i nieobecności ograniczenie szybkości w punktach końcowych uwierzytelniania. Są to problemy, które można rozwiązać za pomocą dobrze znanych wzorców stosowane z dyscypliną.
Jeśli korzystasz z narzędzi do kodowania wspomaganych sztuczną inteligencją, pamiętaj o tym 45% kodu wygenerowanego przez sztuczną inteligencję nie przechodzi testów bezpieczeństwa: Wygenerowany kod często implementuje uwierzytelnianie, ale je pomija kontrola własności zasobów (BOLA), wykorzystuje powierzchowne ograniczanie szybkości oparte wyłącznie na IP, np akceptuje dowolne pola jako dane wejściowe bez sprawdzania poprawności (przypisanie masowe). Skorzystaj z tej listy kontrolnej artykuł jako punkt systematycznej weryfikacji.
Kolejne kroki w serii Bezpieczeństwo sieciowe
- Poprzedni artykuł: Bezpieczne uwierzytelnianie: sesja, pliki cookie i nowoczesna tożsamość - Zarządzanie sesjami, bezpieczne pliki cookie, OAuth 2.1 PKCE i WebAuthn
- Następny artykuł: Bezpieczeństwo łańcucha dostaw: audyt npm i SBOM - audyt npm, Depabot, generowanie SBOM i bezpieczne zarządzanie zależnościami
- Podwaliny: OWASP Top 10 2025 - Pełny przegląd najważniejszych luk w zabezpieczeniach sieciowych, w tym A03 Supply Chain i A10 Error Handling
- Powiązane DevOps: Sprawdź serię Frontend DevOps aby zintegrować zabezpieczenia API z potokiem CI/CD
- Sztuczna inteligencja i bezpieczeństwo: Zobacz serię Kodowanie Vibe aby zrozumieć ryzyko związane z kodem generowanym przez sztuczną inteligencję i jak je systematycznie testować







