La sicurezza di un'applicazione enterprise non è una feature da aggiungere a fine progetto: è una proprietà architetturale che deve essere progettata fin dal primo ADR. Le violazioni dei dati costano in media 4,88 milioni di dollari per incidente nel 2024 secondo IBM — un costo che incorpora remediation tecnica, notifiche agli utenti, sanzioni regolamentari e danni reputazionali duraturi. Eppure la maggior parte delle vulnerabilità sfruttate in produzione non deriva da attacchi sofisticati: deriva da configurazioni errate di protocolli ampiamente documentati, da JWT validati in modo superficiale, da API che espongono più dati di quanto necessario e da policy di autorizzazione mai revisionate dopo il deploy iniziale.
Il fondamento: OAuth 2.0 e OpenID Connect
OAuth 2.0 è il protocollo di autorizzazione standard per le API moderne. OpenID Connect (OIDC) è uno strato di identità costruito su OAuth 2.0 che aggiunge autenticazione. La distinzione è critica e spesso fraintesa: OAuth autorizza l'accesso alle risorse (questo client può leggere gli ordini?), OIDC autentica l'identità dell'utente (chi è questo utente?). Per applicazioni enterprise moderne, il flow corretto è quasi sempre Authorization Code con PKCE — mai Implicit Flow, deprecato per valide ragioni di sicurezza, e mai il trasferimento di credenziali direttamente al client. PKCE (Proof Key for Code Exchange) previene l'authorization code interception attack: anche se un attaccante intercettasse il codice di autorizzazione, non potrebbe scambiarlo per un token senza il code_verifier originale.
Flusso OAuth 2.0 Authorization Code con PKCE
JWT: sicurezza oltre la validazione base
I JSON Web Token sono ovunque, ma la loro semplicità apparente nasconde numerose trappole. Un JWT è firmato (JWS) ma non cifrato per default: il payload è semplicemente base64url-encoded e leggibile da chiunque in possesso del token. Questo significa che non vanno mai inseriti nei claim dati sensibili come password, numeri di carte di credito o informazioni di autorizzazione che non devono essere visibili al client. Per i casi in cui la confidenzialità del payload è necessaria, si usa JWE (JSON Web Encryption). L'attacco 'alg: none' — in cui un attaccante forgia un token impostando l'algoritmo di firma a none — è banale da eseguire su librerie mal configurate: è fondamentale vincolare esplicitamente gli algoritmi accettati.
import { jwtVerify, createRemoteJWKSet } from "jose";
// JWKS remoti: permette la rotazione delle chiavi senza deploy
const JWKS = createRemoteJWKSet(
new URL("https://auth.example.com/.well-known/jwks.json"),
);
interface ValidatedClaims {
sub: string;
email: string;
roles: string[];
aud: string;
iss: string;
exp: number;
}
async function validateAccessToken(token: string): Promise<ValidatedClaims> {
const { payload } = await jwtVerify(token, JWKS, {
// Vincola issuer atteso — previene token da altri authorization server
issuer: "https://auth.example.com",
// Vincola audience attesa — previene audience confusion attacks
audience: "api.example.com",
// Forza algoritmi asimmetrici — previene algorithm confusion (alg: "none")
algorithms: ["RS256", "ES256"],
});
if (!payload.sub || typeof payload.sub !== "string") {
throw new Error("Token privo di subject valido");
}
const roles = Array.isArray(payload["roles"])
? (payload["roles"] as string[])
: [];
return {
sub: payload.sub,
email: payload["email"] as string,
roles,
aud: Array.isArray(payload.aud) ? payload.aud[0] : (payload.aud as string),
iss: payload.iss as string,
exp: payload.exp as number,
};
}
// Middleware per proteggere gli endpoint API
export async function authMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Token mancante" });
}
try {
const token = authHeader.slice(7);
const claims = await validateAccessToken(token);
(req as any).user = claims;
next();
} catch (err) {
// Log dell'errore senza esporre dettagli al client
logger.warn("JWT validation failed", { error: (err as Error).message });
return res.status(401).json({ error: "Token non valido o scaduto" });
}
}- Usa sempre JWKS remoti invece di segreti hardcoded: permette la rotazione delle chiavi senza deploy.
- Vincola esplicitamente gli algoritmi accettati: l'attacco 'alg: none' è banale su librerie mal configurate.
- Imposta exp breve (15-60 minuti) per gli access token e usa refresh token rotation per le sessioni longeve.
- Non inserire PII sensibili nel payload JWT: il contenuto è solo base64, non cifratura.
- Implementa token revocation tramite una blocklist Redis per i casi di logout forzato o compromise.
Modelli di controllo accessi: RBAC, ABAC e il modello ibrido
Role-Based Access Control (RBAC) è il modello più diffuso: ogni utente ha uno o più ruoli e ogni ruolo ha un insieme di permessi. È semplice da implementare e da auditare, ma soffre di 'role explosion' man mano che l'organizzazione cresce. Attribute-Based Access Control (ABAC) valuta le decisioni di accesso basandosi su attributi dell'utente (reparto, livello di seniority), dell'ambiente (orario, posizione geografica, device posture) e della risorsa (classificazione del dato, owner). ABAC è più espressivo ma più complesso da governare. La best practice enterprise moderna è un modello ibrido: RBAC come base per i permessi standard, arricchito con policy ABAC per i casi che richiedono contesto dinamico.
// Modello ibrido RBAC + ABAC con CASL
import {
AbilityBuilder,
createMongoAbility,
MongoAbility,
} from "@casl/ability";
type Actions = "read" | "create" | "update" | "delete" | "approve";
type Subjects = "Order" | "Invoice" | "Report" | "User" | "all";
type AppAbility = MongoAbility<[Actions, Subjects]>;
interface UserContext {
id: string;
roles: string[];
department: string;
clearanceLevel: number; // 1-5
}
export function defineAbilitiesFor(user: UserContext): AppAbility {
const { can, cannot, build } = new AbilityBuilder<AppAbility>(
createMongoAbility,
);
if (user.roles.includes("admin")) {
can("manage", "all");
}
if (user.roles.includes("manager")) {
can("read", "Order");
can("read", "Invoice");
// ABAC: approva solo ordini sotto soglia (attributo della risorsa)
can("approve", "Order", { amount: { $lte: 50000 } });
}
if (user.roles.includes("analyst")) {
can("read", "Report");
can("read", "Order");
// ABAC: crea report solo per il proprio dipartimento
can("create", "Report", { department: user.department });
}
if (user.roles.includes("viewer")) {
can("read", "Order");
// ABAC: leggi solo report con clearance <= clearance utente
can("read", "Report", {
clearanceRequired: { $lte: user.clearanceLevel },
});
}
// Regola esplicita di negazione — audit trail immutabile
cannot("delete", "Invoice");
return build({
detectSubjectType: (item) => item.constructor.name as Subjects,
});
}
export async function approveOrder(req: Request, res: Response) {
const ability = defineAbilitiesFor((req as any).user);
const order = await Order.findById(req.params.id);
if (!order || ability.cannot("approve", order)) {
return res.status(403).json({ error: "Permesso negato" });
}
await order.approve((req as any).user.id);
res.json({ status: "approved" });
}OWASP API Security Top 10: le vulnerabilità da non ignorare
L'OWASP API Security Top 10 è la lista di riferimento per le vulnerabilità più critiche delle API moderne. Le prime tre posizioni — Broken Object Level Authorization (BOLA), Broken Authentication e Broken Object Property Level Authorization — condividono una causa comune: fidarsi dell'input del client per determinare cosa mostrare o modificare. BOLA significa che un utente può leggere o modificare risorse di altri utenti semplicemente cambiando un ID nell'URL. La correzione è sempre verificare, lato server, che la risorsa richiesta appartenga all'utente autenticato — mai assumere che l'ID nel token JWT corrisponda all'ID nell'URL.
- BOLA (API1): verifica sempre che req.user.id === resource.ownerId prima di restituire o modificare dati.
- Broken Authentication (API2): usa PKCE, ruota i refresh token, implementa rate limiting su /token.
- Broken Object Property Level Authorization (API3): usa DTO espliciti e non esporre mai l'intera entity ORM.
- Unrestricted Resource Consumption (API4): rate limiting per IP e per utente su tutti gli endpoint pubblici.
- Broken Function Level Authorization (API5): le route admin devono avere middleware di autorizzazione separato.
- Unrestricted Access to Sensitive Business Flows (API6): limita operazioni critiche con CAPTCHA e anomaly detection.
- Server Side Request Forgery — SSRF (API7): valida e applica una whitelist a qualsiasi URL fornito in input.
- Security Misconfiguration (API8): rimuovi endpoint di debug, disabilita CORS wildcard, aggiorna le dipendenze.
Zero Trust: non fidarsi mai, verificare sempre
Il modello Zero Trust ribalta l'assunzione tradizionale che le reti interne siano sicure. In un'architettura Zero Trust, ogni richiesta — anche proveniente da un servizio interno — viene autenticata e autorizzata esplicitamente. Non esiste una 'zona sicura'. I principi operativi sono: verifica continua dell'identità (ogni chiamata API porta un token verificato), accesso con privilegio minimo (ogni servizio ha solo i permessi strettamente necessari), micro-segmentazione della rete (i servizi non possono comunicare liberamente) e assunzione di breach (monitora e logga tutto come se ci fosse già un attaccante dentro). Service mesh come Istio o Linkerd implementano mutual TLS automaticamente tra i pod Kubernetes, garantendo che ogni comunicazione service-to-service sia cifrata e autenticata.
Crittografia: dati a riposo e in transito
TLS 1.3 è il requisito minimo per tutti i dati in transito — TLS 1.0 e 1.1 sono deprecati e vulnerabili. Per le API interne su Kubernetes, mTLS via service mesh è la soluzione raccomandata. Per i dati a riposo, la strategia dipende dal livello di sensibilità: la cifratura a livello di disco protegge da attacchi fisici ma non dall'applicazione stessa. La cifratura a livello di campo — cifrare specifiche colonne del database con chiavi gestite dall'applicazione tramite AWS KMS o HashiCorp Vault — fornisce difesa in profondità: anche un dump del database non espone i dati sensibili. Le chiavi di cifratura non devono mai essere hardcoded nel codice o nelle variabili di ambiente in chiaro: usa un secret manager dedicato con rotation automatica.
Sicurezza pratica: esegui un threat model esplicito per ogni nuova feature che tocca dati sensibili o flussi di autenticazione. Il modello STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) è un framework efficace per identificare sistematicamente le superfici di attacco prima che diventino vulnerabilità in produzione.
Conclusioni
La sicurezza enterprise non è un prodotto che si acquista o una checklist che si spunta: è una disciplina ingegneristica continua. OAuth 2.0 con PKCE gestisce l'autorizzazione in modo standard e auditabile; JWT validati correttamente riducono drasticamente la superficie di attacco dei token; RBAC con overlay ABAC bilancia semplicità operativa e granularità delle policy; OWASP API Top 10 fornisce la lista di priorità per le revisioni di sicurezza; Zero Trust con mTLS elimina il perimetro come concetto di sicurezza; la crittografia dei dati sensibili con gestione delle chiavi esternalizzata completa la difesa in profondità. Ognuno di questi layer, da solo, è insufficiente. Insieme, formano un'architettura che rende costoso e difficile per un attaccante ottenere accesso significativo anche quando un singolo controllo fallisce.
Tag
Prossimo passo
Hai bisogno di implementare questa architettura?
Il nostro team di ingegneria costruisce e scala i sistemi descritti in questo articolo. Dalla discovery alla produzione — con risultati misurabili.
Articoli correlati