Creo applicazioni web moderne e strumenti digitali personalizzati per aiutare le attività a crescere attraverso l'innovazione tecnologica. La mia passione è unire informatica ed economia per generare valore reale.
La mia passione per l'informatica è nata tra i banchi dell'Istituto Tecnico Commerciale di Maglie, dove ho scoperto il potere della programmazione e il fascino di creare soluzioni digitali. Fin da subito, ho capito che l'informatica non era solo codice, ma uno strumento straordinario per trasformare idee in realtà.
Durante gli studi superiori in Sistemi Informativi Aziendali, ho iniziato a intrecciare informatica ed economia, comprendendo come la tecnologia possa essere il motore della crescita per qualsiasi attività. Questa visione mi ha accompagnato all'Università degli Studi di Bari, dove ho conseguito la Laurea in Informatica, approfondendo le mie competenze tecniche e la mia passione per lo sviluppo software.
Oggi metto questa esperienza al servizio di imprese, professionisti e startup, creando soluzioni digitali su misura che automatizzano processi, ottimizzano risorse e aprono nuove opportunità di business. Perché la vera innovazione inizia quando la tecnologia incontra le esigenze reali delle persone.
Le Mie Competenze
Analisi Dati & Modelli Previsionali
Trasformo i dati in insights strategici con analisi approfondite e modelli predittivi per decisioni informate
Automazione Processi
Creo strumenti personalizzati che automatizzano operazioni ripetitive e liberano tempo per attività a valore aggiunto
Sistemi Custom
Sviluppo sistemi software su misura, dalle integrazioni tra piattaforme alle dashboard personalizzate
Credo fermamente che l'informatica sia lo strumento più potente per trasformare le idee in realtà e migliorare la vita delle persone.
Democratizzare la Tecnologia
La mia missione è rendere l'informatica accessibile a tutti: dalle piccole imprese locali alle startup innovative, fino ai professionisti che vogliono digitalizzare la propria attività. Ogni realtà merita di sfruttare le potenzialità del digitale.
Unire Informatica ed Economia
Non è solo questione di scrivere codice: è capire come la tecnologia possa generare valore reale. Intrecciando competenze informatiche e visione economica, aiuto le attività a crescere, ottimizzare processi e raggiungere nuovi traguardi di efficienza e redditività.
Creare Soluzioni su Misura
Ogni attività è unica, e così devono esserlo le soluzioni. Sviluppo strumenti personalizzati che rispondono alle esigenze specifiche di ciascun cliente, automatizzando processi ripetitivi e liberando tempo per ciò che conta davvero: far crescere il business.
Trasforma la Tua Attività con la Tecnologia
Che tu gestisca un negozio, uno studio professionale o un'azienda, posso aiutarti a sfruttare le potenzialità dell'informatica per lavorare meglio, più velocemente e in modo più intelligente.
Il mio percorso accademico e le tecnologie che padroneggio
Certificazioni Professionali
8 certificazioni conseguite
Nuovo
Visualizza
Reinvention With Agentic AI Learning Program
Anthropic
Dicembre 2024
Nuovo
Visualizza
Agentic AI Fluency
Anthropic
Dicembre 2024
Nuovo
Visualizza
AI Fluency for Students
Anthropic
Dicembre 2024
Nuovo
Visualizza
AI Fluency: Framework and Foundations
Anthropic
Dicembre 2024
Nuovo
Visualizza
Claude with the Anthropic API
Anthropic
Dicembre 2024
Visualizza
Master SQL
RoadMap.sh
Novembre 2024
Visualizza
Oracle Certified Foundations Associate
Oracle
Ottobre 2024
Visualizza
People Leadership Credential
Connect
Settembre 2024
Linguaggi & Tecnologie
Java
Python
JavaScript
Angular
React
TypeScript
SQL
PHP
CSS/SCSS
Node.js
Docker
Git
💼
12/2024 - Presente
Custom Software Engineering Analyst
Accenture
Bari, Puglia, Italia · Ibrida
Analisi e sviluppo di sistemi informatici attraverso l'utilizzo di Java e Quarkus in Health and Public Sector. Formazione continua su tecnologie moderne per la creazione di soluzioni software personalizzate ed efficienti e sugli agenti.
💼
06/2022 - 12/2024
Analista software e Back End Developer Associate Consultant
Links Management and Technology SpA
Esperienza nell'analisi di sistemi software as-is e flussi ETL utilizzando PowerCenter. Formazione completata su Spring Boot per lo sviluppo di applicazioni backend moderne e scalabili. Sviluppatore Backend specializzato in Spring Boot, con esperienza in progettazione di database, analisi, sviluppo e testing dei task assegnati.
💼
02/2021 - 10/2021
Programmatore software
Adesso.it (prima era WebScience srl)
Esperienza nell'analisi AS-IS e TO-BE, evoluzioni SEO ed evoluzioni website per migliorare le performance e l'engagement degli utenti.
🎓
2018 - 2025
Laurea in Informatica
Università degli Studi di Bari Aldo Moro
Bachelor's degree in Computer Science, focusing on software engineering, algorithms, and modern development practices.
📚
2013 - 2018
Diploma - Sistemi Informativi Aziendali
Istituto Tecnico Commerciale di Maglie
Technical diploma specializing in Business Information Systems, combining IT knowledge with business management.
Contattami
Hai un progetto in mente? Parliamone! Compila il form qui sotto e ti risponderò al più presto.
* Campi obbligatori. I tuoi dati saranno utilizzati solo per rispondere alla tua richiesta.
05 - API Security: OAuth 2.1, JWT e Rate Limiting
Le API sono la spina dorsale delle applicazioni moderne. Ogni microservizio, ogni mobile app, ogni integrazione
SaaS passa attraverso endpoint HTTP che espongono dati e funzionalità critiche. Secondo il report
Salt Security State of API Security 2024, gli attacchi alle API sono aumentati del
167% negli ultimi 12 mesi, con il 94% delle organizzazioni che ha subito almeno un
incidente di sicurezza legato alle API nel corso dell'anno. Non si tratta di statistiche lontane dalla
realta quotidiana: la stragrande maggioranza di queste vulnerabilità riguarda pattern che i developer
replicano ogni giorno.
Il problema e strutturale. Le API vengono progettate pensando alla funzionalità, e la sicurezza viene
aggiunta in un secondo momento come strato superficiale. Si aggiunge un token JWT e si considera il
lavoro finito. Ma l'OWASP API Security Top 10:2023 mostra che i vettori più critici non sono tecnici:
sono logici. Broken Object Level Authorization, Broken Function Level Authorization, Unrestricted
Resource Consumption. Vulnerabilità che nessun framework risolve automaticamente, perchè richiedono
decisioni architetturali deliberate.
Questo articolo ti guida attraverso l'intera superficie di attacco delle API moderne: dall'OWASP API
Top 10:2023 al rate limiting con algoritmi token bucket e sliding window, da OAuth 2.1 con scopes
granulari alla validazione dell'input, dalla configurazione CORS corretta ai pattern di API gateway.
Codice pratico Node.js/Express per ogni sezione, con una checklist finale specifica per Angular.
Cosa Imparerai
OWASP API Security Top 10:2023: le 10 vulnerabilità più critiche con esempi pratici
Rate limiting: implementazione token bucket e sliding window con Redis in Node.js
OAuth 2.1 con scopes granulari: differenze da OAuth 2.0 e PKCE obbligatorio
JWT best practices: algoritmi sicuri, rotazione dei token, revoca e blacklisting
API keys vs Bearer token: quando usare quale approccio e come gestirle in sicurezza
Input validation e sanitization con Zod per TypeScript
Configurazione CORS sicura: whitelist origini e gestione credenziali
API gateway patterns: autenticazione centralizzata, circuit breaker, logging sicuro
Monitoring e alerting: rilevare pattern di attacco in tempo reale
Checklist Angular per chiamate API sicure dal frontend
OWASP API Security Top 10:2023
L'OWASP API Security Top 10:2023 e la lista di riferimento per comprendere i rischi più critici
nelle API moderne. Rispetto alla versione 2019, introduce tre nuove categorie e riordina le priorità
sulla base di dati reali di incidenti. Non e una lista teorica: ogni voce corrisponde a vulnerabilità
documentate che hanno causato breach reali negli ultimi anni.
BOLA e il numero uno da tre edizioni consecutive e rappresenta circa il 40% di tutti gli attacchi
API documentati. L'API restituisce risorse identificate da un ID senza verificare che l'utente
autenticato abbia i diritti su quella specifica risorsa. E la versione API del classico IDOR
(Insecure Direct Object Reference): il client chiede /api/invoices/1234 e il server
risponde senza verificare che la fattura 1234 appartenga all'utente che la sta richiedendo.
// 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 - Broken Authentication
Autenticazione rotta va oltre la semplice mancanza di login. Include token JWT con algoritmo
none, chiavi di firma deboli, mancata validazione dell'audience (aud) e
dell'issuer (iss), token che non scadono mai, e mancanza di protezione brute-force
sugli endpoint di login. Un attaccante che trova un JWT firmato con HS256 e una
chiave debole come secret può riassegnarsi qualsiasi ruolo.
Nuova nella 2023, BOPLA unisce due vulnerabilità precedenti: Excessive Data Exposure
(l'API restituisce più campi del necessario, inclusi dati sensibili) e Mass Assignment
(l'API accetta campi che non dovrebbe, permettendo all'utente di modificare isAdmin
o role). Il pattern sicuro e la proiezione esplicita dei campi sia in input che in output.
// 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 - Unrestricted Resource Consumption
Sostituto di "Lack of Resources and Rate Limiting", questa categoria copre tutti gli scenari in
cui un'API non limita il consumo di risorse: richieste senza paginazione che restituiscono milioni
di record, upload di file senza limiti di dimensione, query GraphQL con depth illimitata, webhook
senza timeout, e operazioni CPU-intensive senza throttling. Il rate limiting da solo non basta:
servono limiti a ogni livello della pipeline.
API5 - API10: Le Altre Vulnerabilità Critiche
API5 - Broken Function Level Authorization: Endpoint amministrativi accessibili a utenti normali (spesso nascosti nella documentazione ma non protetti dal codice)
API6 - Unrestricted Access to Sensitive Business Flows: Abuso di flussi legittimi come creazione massiva di account, acquisti automatizzati o invio di OTP in bulk
API7 - Server Side Request Forgery (SSRF): L'API accetta URL forniti dal client e li richiede server-side, permettendo di raggiungere servizi interni
API8 - Security Misconfiguration: Headers mancanti, CORS wildcard, debug mode in produzione, versioni API obsolete esposte senza autenticazione
API9 - Improper Inventory Management: API versioni deprecate non rimosse, endpoint di test in produzione, shadow API non documentate
API10 - Unsafe Consumption of APIs: Fiducia cieca in API di terze parti senza validazione dell'output e gestione degli errori
Rate Limiting: Algoritmi e Implementazione
Il rate limiting e il meccanismo che previene l'abuso delle API limitando il numero di richieste
che un client può effettuare in un intervallo di tempo. Non e solo una misura anti-DDoS: protegge
da credential stuffing, scraping, enumerazione di risorse e abuso dei flussi di business. La scelta
dell'algoritmo influenza la user experience e la protezione effettiva.
Token Bucket Algorithm
Il token bucket e l'algoritmo più comune per il rate limiting. Ogni client ha un "secchio" con
capacità massima N token. I token vengono aggiunti a una velocità costante R token al secondo.
Ogni richiesta consuma un token. Se il secchio e vuoto, la richiesta viene rifiutata con HTTP 429.
Il vantaggio e che permette burst di traffico fino alla capacità del secchio,
poi stabilizza a R richieste al secondo. Ideale per API pubbliche con traffico a ondate naturali.
// 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:
#123;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:#123;(req as any).user.id}`
: `ip:#123;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 #123;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
});
Sliding Window Algorithm
Il fixed window ha un problema noto: un attaccante può concentrare N richieste alla fine di una
finestra e N richieste all'inizio di quella successiva, ottenendo 2N richieste in un breve lasso
di tempo senza violare il limite tecnico. Lo sliding window risolve questo mantenendo un conteggio
esatto per ogni finestra temporale che "scorre" con il tempo, usando un Redis Sorted Set dove ogni
elemento ha come score il timestamp della richiesta.
// 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:#123;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, `#123;now}:#123;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:#123;(req as any).user!.id}` // Per-user invece che per-IP
)
);
Token Bucket vs Sliding Window: Guida alla Scelta
Criterio
Token Bucket
Sliding Window
Burst di traffico
Permette burst fino alla capacità del secchio
Applica limite uniforme, no burst
Precisione del limite
Approssimativa (dipende dal refill rate)
Precisa al millisecondo
Storage Redis
2 valori per chiave (hash leggero)
N elementi per chiave (sorted set, proporzionale al traffico)
Comportamento ai picchi
Assorbe picchi naturali senza errori
Rigido: oltre il limite restituisce 429
Caso d'uso ideale
API pubbliche, CDN, traffico utente variabile
Endpoint critici: auth, pagamenti, OTP
OAuth 2.1 e JWT: Autenticazione API Moderna
OAuth 2.1 (RFC draft) consolida le best practice di sicurezza di OAuth 2.0. Le differenze chiave
rispetto a OAuth 2.0 sono: PKCE obbligatorio per tutti i client (non solo public
clients), implicit flow rimosso, resource owner password credentials
flow rimosso, e exact redirect URI matching obbligatorio (niente
pattern matching con wildcard). Se la tua implementazione OAuth 2.0 già seguiva le OWASP Security
Cheat Sheet, il passaggio a OAuth 2.1 richiede modifiche minime.
Scopes Granulari: Minimo Privilegio per le API
Gli scopes OAuth definiscono cosa un token può fare. Scopes troppo ampi come read:all
o admin violano il principio del minimo privilegio. Un token rubato con scope
invoices:read:own causa un danno molto più limitato rispetto a uno con
read:all. La granularità degli scopes deve riflettere le operazioni reali dell'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: I 6 Errori Fatali
JWT e ampiamente diffuso ma altrettanto ampiamente mal implementato. Questi sei pattern errati
trasformano un meccanismo di autenticazione solido in una vulnerabilità critica. Ogni punto
corrisponde a una CVE documentata o a un pattern di attacco reale.
// 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
API Keys: Gestione Sicura per Integrazioni Server-to-Server
Le API keys sono la scelta giusta per comunicazioni machine-to-machine dove il client e
controllato dal proprietario dell'account (script di automazione, backend di terze parti,
integrazioni CI/CD). Sono più semplici di OAuth ma richiedono gestione attenta: una API key
deve essere trattata come una password, mai esposta in log o codice sorgente.
// 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_#123;env}_#123;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.',
});
});
Input Validation con Zod
La validazione dell'input e la prima linea di difesa contro injection, mass assignment e
comportamenti imprevisti. Per API TypeScript, Zod e il tool di riferimento nel 2025:
definisce lo schema una volta e ottieni validazione runtime più tipi TypeScript inferiti
automaticamente, senza duplicazione tra interfacce TypeScript e validatori runtime. Ogni endpoint
deve validare body, params e query string separatamente.
// 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);
}
);
Configurazione CORS Sicura
CORS e spesso la prima configurazione che un developer tocca quando le chiamate API dal frontend
falliscono in sviluppo. La risposta più comune e sbagliata: Access-Control-Allow-Origin: *.
Questo disabilita la protezione same-origin per tutta la tua API. Peggior ancora,
Access-Control-Allow-Origin: * con credentials: true non funziona affatto
per i browser moderni (per sicurezza) e dovrebbe sollevare un alert immediato nel code review.
// 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 '#123;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' },
}));
API Gateway Patterns per la Sicurezza
Un API gateway centralizza preoccupazioni trasversali come autenticazione, rate limiting, logging
e trasformazione delle richieste. In un'architettura a microservizi, evita di replicare la logica
di sicurezza in ogni servizio. La composizione del middleware Express deve seguire un ordine preciso:
security headers prima di ogni elaborazione, poi rate limiting, poi autenticazione, poi validazione,
infine le route.
// 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:#123;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: #123;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');
});
Monitoring: Rilevare gli Attacchi in Tempo Reale
Un sistema di monitoring efficace per le API si concentra su metriche di sicurezza specifiche:
tassi di errore 401/403 per IP e per endpoint, pattern di enumerazione sugli ID delle risorse,
richieste anomale per dimensione del payload, e accessi a endpoint non documentati o deprecati.
Le metriche applicative generali non bastano: servono contatori orientati alla sicurezza.
// 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 = `#123;ip}:#123;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:#123;ip}`, 3600, '1');
console.warn(`[SECURITY] Enumeration detected from #123;ip}, endpoint: #123;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());
});
Checklist Angular per API Security
Il frontend Angular ha responsabilità specifiche nella sicurezza delle API. La scelta di dove
memorizzare i token ha conseguenze dirette sulla superficie di attacco XSS. L'interceptor HTTP
centralizza la gestione della sicurezza evitando di distribuire logica critica in decine di service.
// 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 #123;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 #123;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 #123;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]))
Checklist Sicurezza API - Angular + Node.js
Area
Controllo
Priorità
Autorizzazione Oggetti
Verifica BOLA: ogni query filtra per owner prima di restituire dati
Access token in-memory Angular, refresh token in HttpOnly cookie SameSite=Strict
CRITICA
Rate Limiting
Token bucket per API generica, sliding window per auth/pagamenti
ALTA
Input Validation
Zod su body/params/query, limiti lunghezza, enum per valori predefiniti
ALTA
CORS
Whitelist origini esplicita, no wildcard con credentials
ALTA
Scopes OAuth
Granulari per risorsa:operazione:contesto, verificati su ogni endpoint
ALTA
Error Handling
No stack trace in produzione, correlation ID per supporto
MEDIA
Monitoring
Contatori 401/403/429, rilevamento enumerazione, alert su anomalie
MEDIA
API Keys
Solo hash nel DB, mai in chiaro; timing-safe comparison; prefisso per identificazione
MEDIA
Anti-Pattern da Evitare Assolutamente
JWT in localStorage: Accessibile da qualsiasi script JS sulla pagina. Usa in-memory per l'access token e HttpOnly cookie per il refresh token
CORS wildcard con credentials:Access-Control-Allow-Origin: * con credentials: true viene rifiutato dai browser moderni per design; indica una misconfiguration
Rate limiting solo per IP: Una botnet distribuisce gli attacchi su migliaia di IP. Combina rate limiting per IP con rate limiting per user ID
Scopes troppo ampi:admin o read:all violano il minimo privilegio. Un token rubato con scope invoices:read:own causa un danno nettamente inferiore
Stack trace esposto in produzione: Rivela path interni, versioni librerie, struttura DB. Usa sempre un error handler che oscura i dettagli in produzione
Algoritmo JWT non specificato: Specifica sempre algorithms: ['RS256']. Alcune librerie JWT con configurazione di default accettano l'algoritmo none
BOLA non verificata: Il 40% degli attacchi API sfrutta BOLA. Ogni endpoint che accetta un ID deve verificare che l'utente sia il proprietario della risorsa
Conclusioni
La sicurezza delle API non si risolve con un singolo strumento o una configurazione. E una pratica
continua che richiede attenzione a ogni livello: dalla progettazione degli endpoint (verificare BOLA
sistematicamente, scopes granulari), all'implementazione del rate limiting (scegliendo l'algoritmo
giusto per ogni use case), alla gestione corretta di OAuth 2.1 e JWT (RS256, scadenze brevi,
refresh token rotation), fino al monitoring per rilevare pattern di attacco prima che causino danni.
I dati del 2024-2025 mostrano che la maggior parte degli incidenti API non riguarda vulnerabilità
esotiche: riguarda errori di autorizzazione di base (non verificare che la risorsa appartiene
all'utente che la richiede), configurazioni CORS permissive, token JWT senza scadenza e assenza
di rate limiting sugli endpoint di autenticazione. Sono problemi risolvibili con pattern ben noti
applicati con disciplina.
Se utilizzi strumenti di AI-assisted coding, ricorda che il 45% del codice generato da AI
fallisce i security test: il codice generato spesso implementa l'autenticazione ma omette
il check di ownership sulle risorse (BOLA), usa rate limiting superficiale basato solo su IP, e
accetta campi arbitrari in input senza validazione (mass assignment). Usa la checklist di questo
articolo come punto di verifica sistematica.