Quando si parla di sicurezza in ASP.NET Core, la parte difficile non è ricordarsi di attivare HTTPS o predisporre i Security Headers (di cui abbiamo già diffusamente parlato in passato): la parte difficile è trattare la sicurezza come un requisito trasversale, che tocca file upload, gestione dei secret, token antiforgery, header HTTP, cifratura applicativa e protezione dagli abusi. Sono tutti punti in cui un'applicazione apparentemente solida può iniziare a cedere, spesso non per una vulnerabilità “clamorosa”, ma per una serie di scelte troppo permissive.
In questo articolo vediamo un set di best practice concrete per rafforzare una web app ASP.NET Core, con un taglio volutamente operativo. L’idea non è tanto quella di mettere in piedi una checklist di best practices, ma ragionare in modo più generale su quali sono le strategie e gli strumenti offerti dal framework per mettere in sicurezza i punti di ingresso più esposti, e quali convenga usare davvero.
Threat model: la mappa delle minacce
Prima di parlare di codice, conviene esplicitare il modello di minaccia. Senza questo passaggio si finisce facilmente per investire tempo dove serve poco, trascurando invece le aree realmente esposte. In una tipica applicazione ASP.NET Core che gestisce autenticazione, upload di file, aree amministrative e API HTTP, gli scenari più comuni sono questi:
- upload di contenuti non verificati, inclusi file malevoli, file con MIME spoofato, documenti corrotti, archivi compressi ostili o payload costruiti per mettere in crisi parser e librerie;
- furto o riuso improprio di cookie e richieste cross-site verso endpoint che accettano operazioni mutanti;
- brute force e abuso di endpoint sensibili, soprattutto login, reset password, API pubbliche ed endpoint ad alto costo computazionale;
- esposizione accidentale di secret applicativi, chiavi API, connection string e materiale crittografico;
- errori di configurazione lato reverse proxy o header HTTP insufficienti, che lasciano aperta una superficie d’attacco evitabile;
- accessi legittimi ma fuori perimetro, che richiedono audit e tracciabilità, non solo autenticazione.
Al tempo stesso è bene essere onesti sui limiti: se l’host è compromesso a livello sistema operativo, un attaccante con privilegi elevati può arrivare alle chiavi di Data Protection, ai secret presenti in memoria e, in generale, a tutto ciò che il processo può leggere. Quel livello di difesa richiede controlli infrastrutturali, non solo applicativi.
Upload security
Gli upload sono uno dei punti più delicati di qualsiasi applicazione web. Il motivo è semplice: stiamo accettando input arbitrario, spesso di grandi dimensioni, che dovrà essere memorizzato, analizzato, indicizzato o elaborato da qualche componente a valle. Fidarsi del nome del file o del content type inviato dal browser è un errore classico.
Allowlist di estensioni e MIME type
La prima difesa sensata è un controllo esplicito su estensioni consentite e content type ammessi. Conviene usare un’allowlist, non una denylist: i formati supportati devono essere pochi, dichiarati e comprensibili. Una denylist, oltre a diventare presto incompleta, tende ad allargarsi in modo caotico.
Questa verifica dovrebbe avvenire il prima possibile, idealmente prima di persistere il contenuto in modo definitivo. In molti progetti ho visto il controllo spostato troppo tardi, magari dopo il salvataggio temporaneo su disco; funziona, ma aumenta inutilmente la superficie operativa.
|
1 2 3 4 5 6 |
public sealed class UploadSecurityOptions { public string[] AllowedExtensions { get; set; } = Array.Empty<string>(); public string[] AllowedContentTypes { get; set; } = Array.Empty<string>(); public long MaxFileSizeBytes { get; set; } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public static void ValidateUpload( IFormFile file, UploadSecurityOptions options) { var extension = Path.GetExtension(file.FileName); if (string.IsNullOrWhiteSpace(extension) || !options.AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { throw new InvalidOperationException("File extension not allowed."); } if (string.IsNullOrWhiteSpace(file.ContentType) || !options.AllowedContentTypes.Contains(file.ContentType, StringComparer.OrdinalIgnoreCase)) { throw new InvalidOperationException("Content type not allowed."); } if (file.Length <= 0 || file.Length > options.MaxFileSizeBytes) { throw new InvalidOperationException("Invalid file size."); } } |
Questo controllo da solo non basta, perché il content type può essere falsificato e anche l’estensione non offre garanzie forti. Resta però un filtro iniziale utile, soprattutto per scartare contenuti evidentemente fuori policy.
Controlli sul contenuto e antimalware
Se l’applicazione accetta documenti provenienti da utenti, clienti o integrazioni esterne, la scansione malware dovrebbe entrare nel flusso standard. ClamAV è una scelta frequente, soprattutto in ambienti Linux o containerizzati, ma il principio resta valido anche con altri motori: il file va analizzato prima che entri nel circuito normale di elaborazione.
Una scelta importante riguarda il comportamento in caso di scanner non disponibile. In produzione, nella maggior parte dei casi, ha più senso un approccio fail-closed: se lo scanner non risponde, l’upload viene rifiutato. Il fail-open può avere senso solo in ambienti di sviluppo o in scenari molto specifici, e comunque va deciso in modo esplicito.
Un flusso robusto assomiglia a questo:
- validazione iniziale di estensione, MIME type e dimensione;
- eventuale salvataggio in area temporanea o streaming verso uno scanner;
- quarantena o rifiuto immediato in caso di esito sospetto;
- solo dopo, persistenza definitiva e pipeline di elaborazione.
Streaming, dimensioni massime e timeout
Un altro errore frequente è leggere tutto in memoria perché “tanto i file saranno piccoli”. È una scommessa che prima o poi si perde. Gli upload vanno trattati in streaming quando possibile, con limiti dimensionali chiari, sia lato ASP.NET Core sia lato reverse proxy.
Se l’app supporta anche ingestione da URL, i controlli devono essere ancora più severi: timeout stretti, dimensione massima del contenuto scaricabile, blocco dell’HTTP in chiaro salvo necessità eccezionali, attenzione a redirect e SSRF. È un’area dove molti sistemi diventano troppo permissivi senza accorgersene.
|
1 2 3 4 |
builder.Services.Configure<FormOptions>(options => { options.MultipartBodyLengthLimit = 25 * 1024 * 1024; }); |
Sanitizzazione del nome file e storage separato
Il nome originale del file non andrebbe mai usato come identificatore fisico sullo storage. Conviene generare un nome interno, usare un path non derivato dall’input utente e conservare il filename originale solo come metadato. È una misura semplice, ma evita collisioni, traversal mal gestiti e altri problemi inutili.
Se possibile, lo storage dei file caricati dovrebbe stare fuori dalla web root. Servire direttamente file caricati dagli utenti come asset statici è comodo, ma crea più problemi di quanti ne risolva.
Secret handling
Una parte rilevante degli incidenti applicativi nasce da qui. Token API finiti nel repository, connection string copiate in chiaro, file di configurazione duplicati su ambienti diversi, chiavi lasciate in log o telemetria. In ASP.NET Core la strada giusta è abbastanza chiara: i secret devono arrivare da provider dedicati e attraversare la pipeline di configurazione senza scorciatoie improvvisate.
Ambienti di sviluppo, CI/CD e produzione
In locale, User Secrets è spesso la scelta più pulita per i developer; in ambienti containerizzati o di pipeline si possono usare environment variable; in produzione conviene appoggiarsi a un secret store vero, come Azure Key Vault. La cosa importante è mantenere una catena di override coerente: le chiavi di configurazione dovrebbero restare le stesse, cambiando solo il provider.
|
1 2 3 4 5 6 7 8 9 |
builder.Configuration .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) .AddEnvironmentVariables(); if (builder.Environment.IsDevelopment()) { builder.Configuration.AddUserSecrets<Program>(); } |
In produzione, se usi Azure Key Vault, l’integrazione va nella stessa direzione:
|
1 2 3 4 5 6 7 8 9 10 |
using Azure.Identity; var keyVaultUri = builder.Configuration["KeyVault:VaultUri"]; if (!string.IsNullOrWhiteSpace(keyVaultUri)) { builder.Configuration.AddAzureKeyVault( new Uri(keyVaultUri), new DefaultAzureCredential()); } |
Encryption dei secret applicativi
Ci sono casi in cui un secret non è solo configurazione di runtime, ma dato applicativo da memorizzare: per esempio API key fornite dall’utente, credenziali verso servizi terzi, token di integrazione per tenant diversi. In questi scenari non basta dire “lo tengo in configurazione”, perché il dato finisce in database.
Qui ha senso usare cifratura applicativa forte, ad esempio AES-GCM, separando chiaramente la chiave master dai dati cifrati. Va anche previsto il tema della rotazione: se cambi la chiave, devi sapere come re-cifrare il materiale esistente senza rompere tutto il parco dati.
Un dettaglio che merita attenzione: il secret in chiaro non dovrebbe mai finire nei log, in serializzazione diagnostica, in audit generici o in eccezioni rilanciate senza filtro. È più comune di quanto sembri.
ASP.NET Core Data Protection
Molti associano Data Protection solo ai cookie di autenticazione e ai token antiforgery, ma il sistema è utile anche in scenari custom, purché venga usato con criterio. La prima cosa da fare è configurare bene il key ring, soprattutto in produzione e ancor più in deployment multi-instance.
Persistenza delle chiavi e isolamento applicativo
Su una singola istanza, salvare le chiavi su file system persistente può bastare. In un cluster, le istanze devono condividere il key ring, altrimenti ogni nodo inizierà a emettere materiale che gli altri non sanno decifrare. Inoltre conviene impostare un nome applicativo esplicito, così da evitare condivisioni accidentali tra applicazioni diverse.
|
1 2 3 |
builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo("/var/app/keys")) .SetApplicationName("MyCompany.MyApp"); |
Se l’app gira in Azure o in ambienti più strutturati, conviene valutare storage centralizzati e protezione delle chiavi con servizi dedicati.
IDataProtector.CreateProtector per scenari custom
Quando devi proteggere un valore applicativo che non richiede una cifratura “portabile” tra stack diversi, Data Protection è spesso più comodo di una soluzione crittografica custom. Penso, per esempio, a token monouso per workflow interni, riferimenti opachi da esporre in URL, identificatori sensibili da serializzare in modo protetto, piccoli payload temporanei firmati e cifrati dal server.
In questi casi IDataProtectionProvider.CreateProtector() permette di definire uno scopo preciso, che separa il materiale protetto per contesto d’uso. È un aspetto fondamentale: due protector con purpose diversi non devono essere intercambiabili.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public sealed class ProtectedLinkService { private readonly IDataProtector _protector; public ProtectedLinkService(IDataProtectionProvider provider) { _protector = provider.CreateProtector("ProtectedLinks.DownloadToken.v1"); } public string Protect(string value) { return _protector.Protect(value); } public string Unprotect(string protectedValue) { return _protector.Unprotect(protectedValue); } } |
Per token a scadenza conviene usare il time-limited protector:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection.Extensions; public sealed class ExpiringTokenService { private readonly ITimeLimitedDataProtector _protector; public ExpiringTokenService(IDataProtectionProvider provider) { _protector = provider .CreateProtector("ExpiringTokens.PasswordReset.v1") .ToTimeLimitedDataProtector(); } public string Issue(string payload, TimeSpan lifetime) { return _protector.Protect(payload, lifetime); } public string Read(string token) { return _protector.Unprotect(token, out _); } } |
Non userei Data Protection per tutto. Se devi interoperare con altri sistemi, gestire rotazioni complesse o controllare in dettaglio formato e algoritmi, una cifratura esplicita può essere più adatta. Per scenari interni all’applicazione, però, è uno strumento molto utile e spesso sottovalutato.
CSRF in ASP.NET Core
La protezione CSRF resta necessaria ogni volta che l’app usa cookie di autenticazione e accetta richieste che modificano stato. Non è un tema “vecchio”: è semplicemente meno visibile di altre vulnerabilità, quindi qualcuno tende a trascurarlo quando passa a SPA ibride, pannelli admin o aree MVC meno frequentate.
[ValidateAntiForgeryToken] vs AutoValidateAntiforgeryTokenAttribute
Qui conviene essere pratici. [ValidateAntiForgeryToken] valida il token antiforgery solo dove lo applichi esplicitamente. Va bene quando vuoi un controllo puntuale, magari su singole action o controller, ma richiede disciplina costante. In team numerosi o in codebase che crescono nel tempo, è facile dimenticarlo su qualche POST, PUT o DELETE.
AutoValidateAntiforgeryTokenAttribute, invece, applicato globalmente, è quasi sempre la scelta più sicura per applicazioni MVC e Razor Pages che usano cookie. Valida automaticamente le richieste mutanti, lasciando stare le GET, HEAD, OPTIONS e TRACE. In altre parole, sposta la protezione da opt-in a default sensato.
|
1 2 3 4 |
builder.Services.AddControllersWithViews(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); }); |
Quando ha senso usare ancora [ValidateAntiForgeryToken]? Nei casi in cui vuoi marcare in modo esplicito endpoint particolarmente sensibili o in codebase dove non puoi introdurre subito una policy globale. Per progetti nuovi, la protezione globale è quasi sempre preferibile.
Due osservazioni pratiche:
- se l’app usa bearer token in header Authorization e non cookie, il rischio CSRF cambia radicalmente e spesso non si applica nello stesso modo;
- nelle applicazioni ibride, con frontend JavaScript che chiama endpoint protetti da cookie, va gestito correttamente anche il token antiforgery lato client.
Esempio di configurazione antiforgery
|
1 2 3 4 5 6 7 8 |
builder.Services.AddAntiforgery(options => { options.HeaderName = "X-CSRF-TOKEN"; options.Cookie.Name = "__Host-antiforgery"; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.HttpOnly = true; options.Cookie.SameSite = SameSiteMode.Strict; }); |
La parte delicata non è tanto attivare il servizio, quanto usarlo in modo coerente con il tipo di client che chiama l’applicazione.
Rate limiting
ASP.NET Core mette a disposizione un middleware di rate limiting molto valido, che vale la pena usare seriamente. Limitare il traffico non serve solo contro i denial of service “puri”; serve anche a frenare brute force, enumeration, scraping aggressivo e consumo eccessivo di endpoint costosi.
Una singola policy globale raramente basta. In genere conviene separare almeno:
- endpoints di autenticazione;
- API pubbliche o semipubbliche;
- operazioni particolarmente costose, ad esempio export, parsing, upload o reportistica.
Sliding window e token bucket
Le policy più interessanti, nella pratica, sono spesso sliding window e token bucket.
La sliding window è utile quando vuoi evitare gli effetti un po’ rigidi del fixed window. Distribuisce meglio i limiti nel tempo e riduce il classico problema dei burst a cavallo tra due finestre consecutive.
Il token bucket è molto adatto quando vuoi consentire burst controllati, mantenendo però un rate medio sostenibile. Per molte API pubbliche è una scelta più naturale del fixed window, perché fotografa meglio il traffico reale.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System.Threading.RateLimiting; builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddSlidingWindowLimiter("auth-sliding", limiterOptions => { limiterOptions.PermitLimit = 10; limiterOptions.Window = TimeSpan.FromMinutes(1); limiterOptions.SegmentsPerWindow = 6; limiterOptions.QueueLimit = 0; limiterOptions.AutoReplenishment = true; }); options.AddTokenBucketLimiter("api-token-bucket", limiterOptions => { limiterOptions.TokenLimit = 100; limiterOptions.TokensPerPeriod = 20; limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(10); limiterOptions.QueueLimit = 0; limiterOptions.AutoReplenishment = true; }); }); |
Poi le policy si applicano per endpoint o gruppo di endpoint:
|
1 2 3 4 5 |
app.MapGroup("/api") .RequireRateLimiting("api-token-bucket"); app.MapPost("/account/login", LoginHandler) .RequireRateLimiting("auth-sliding"); |
Partitioning e chiave corretta
La qualità della policy dipende molto da come partizioni il traffico. Limitare solo per IP è meglio di niente, ma dietro NAT, reverse proxy o reti aziendali condivise può essere troppo grezzo. In alcuni casi ha più senso combinare IP, user identity, client id o tenant id. L’importante è non affidarsi a header facilmente spoofabili se non sono già stati bonificati correttamente dal proxy a monte.
Risposte coerenti e Retry-After
Quando una richiesta viene rifiutata, è utile restituire un 429 chiaro e, dove possibile, un header Retry-After. Se l’app usa Problem Details, conviene uniformare anche questo caso, così da non avere errori speciali con formato diverso dal resto dell’API.
HTTP hardening: HSTS, CSP e Security Headers
Gli header HTTP non sostituiscono la sicurezza applicativa, ma aiutano parecchio a ridurre rischi evitabili e a imporre comportamenti più stretti lato browser. In ASP.NET Core, quelli da trattare con più attenzione sono almeno HSTS e CSP.
HSTS
HSTS dice al browser di usare solo HTTPS per il dominio interessato, evitando downgrade involontari o tentativi di accesso in chiaro dopo la prima visita. In produzione ha molto senso, purché l’app sia davvero pronta a vivere sempre e solo in HTTPS.
|
1 2 3 4 |
if (!app.Environment.IsDevelopment()) { app.UseHsts(); } |
|
1 2 3 4 5 6 |
builder.Services.AddHsts(options => { options.MaxAge = TimeSpan.FromDays(180); options.IncludeSubDomains = true; options.Preload = false; }); |
Con IncludeSubDomains e soprattutto con il preload conviene essere prudenti: una configurazione troppo aggressiva, se l’infrastruttura non è uniforme, crea più problemi che benefici.
Content-Security-Policy
CSP è uno degli strumenti più efficaci per ridurre l’impatto di XSS e caricamenti indesiderati, ma funziona bene solo se scritta con cura. Una policy permissiva del tipo 'unsafe-inline' quasi ovunque serve a poco. La costruzione della CSP richiede spesso un piccolo lavoro di pulizia sul frontend, specialmente in applicazioni legacy o in pagine Razor molto vecchie.
Un esempio minimale, da adattare al proprio caso:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
app.Use(async (context, next) => { context.Response.Headers["Content-Security-Policy"] = "default-src 'self'; " + "script-src 'self'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; " + "object-src 'none'; " + "base-uri 'self'; " + "frame-ancestors 'none'; " + "form-action 'self'"; context.Response.Headers["X-Content-Type-Options"] = "nosniff"; context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; context.Response.Headers["X-Frame-Options"] = "DENY"; await next(); }); |
Su X-Frame-Options vale la pena notare che frame-ancestors nella CSP è più moderno e flessibile; tenerli entrambi, in molti contesti, non è un problema. La CSP, però, dovrebbe diventare la fonte principale di controllo.
Forwarded headers e reverse proxy
Se l’app gira dietro Nginx, Apache, YARP, Azure Front Door o altri reverse proxy, la configurazione dei forwarded headers è cruciale. Una cattiva configurazione può alterare schema, IP client e altri dati usati poi da autenticazione, redirect, logging e rate limiting.
ASP.NET Core va configurato con attenzione, limitando proxy e network fidati. Accettare ciecamente catene di header inoltrati è un invito a ricevere dati falsificati.
|
1 2 3 4 5 6 7 8 9 10 11 |
using Microsoft.AspNetCore.HttpOverrides; using System.Net; builder.Services.Configure<ForwardedHeadersOptions>(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.KnownProxies.Add(IPAddress.Parse("10.0.0.10")); options.ForwardLimit = 1; }); |
|
1 |
app.UseForwardedHeaders(); |
Questo passaggio incide anche sul rate limiting per IP: se gli header inoltrati non sono affidabili, limiterai il proxy e non il client reale, oppure peggio ancora accetterai IP manipolati.
Cookie di autenticazione
Quando l’app usa cookie di autenticazione, alcune impostazioni dovrebbero essere considerate default sensati: Secure, HttpOnly, SameSite coerente con il flusso applicativo. Non è la parte più glamour della sicurezza, ma continua a essere una delle più utili.
|
1 2 3 4 5 6 7 |
builder.Services.ConfigureApplicationCookie(options => { options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Lax; options.SlidingExpiration = true; }); |
SameSite=Strict è più restrittivo, ma non sempre compatibile con tutti i flussi di login o navigazione. Lax resta spesso un compromesso pragmatico per il cookie di autenticazione, mentre cookie più sensibili possono meritare policy ancora più strette.
Audit trail: non serve solo per compliance
Registrare eventi di sicurezza in modo coerente aiuta a capire cosa è successo davvero quando qualcosa va storto. Login riusciti, accessi negati, upload accettati o rifiutati, job saltati per motivi di sicurezza, errori applicativi con trace identifier: tutto questo è utile sia in fase di incident response sia nella normale manutenzione.
La cosa importante è evitare un audit rumoroso e poco leggibile. Meglio pochi eventi significativi, con metadati utili e consistenti, che un fiume di record ingestibili. Naturalmente l’audit non deve mai includere il contenuto dei secret o dati sensibili non necessari.
Conclusioni
In molti progetti i temi trattati in questo articolo vengono affrontati tardi, spesso solo dopo il primo incidente o dopo un security review che evidenzia problematiche simili a quelle descritte nei paragrafi precedenti. Ridursi ad aspettare quel momento può essere fatale: è decisamente meglio muoversi prima, partendo con policy restrittive (secondo un approccio che viene spesso chiamato Security by Default) e allentando solo dove serve davvero. In molti casi è sufficiente implementare un pacchetto di contromisure "minimali": allowlist per gli upload, magari scanner integrato nel flusso; antiforgery globale per le richieste mutanti; Data Protection configurata bene; header HTTP espliciti; rate limiting per endpoint sensibili, e possibilmente una gestione seria dei secret. Questi accorgimenti, se implementati bene, evitano una quantità sorprendente di problemi e grattacapi, consentendovi di dormire sonni (relativamente) tranquilli.
La buona notizia è che, come mostrato in questo articolo, ASP.NET Core mette già a disposizione buona parte degli strumenti necessari: la differenza, come spesso accade, la fanno le scelte di configurazione e il rigore con cui vengono mantenute quando l’app cresce. Ricordiamoci sempre che molte vulnerabilità applicative non nascono da un singolo errore grave, ma da una serie di piccole "concessioni" alla sicurezza compiute nel corso del tempo: lo scopo di queste best practice è proprio quello impedire a queste pericolosissime deroghe di accumularsi.
Riferimenti
- ASP.NET Core security documentation - Panoramica ufficiale sulle funzionalità di sicurezza del framework.
- Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core - Documentazione su antiforgery token, filtri e integrazione lato applicazione.
- Rate limiting middleware in ASP.NET Core - Guida ufficiale alle policy fixed window, sliding window, token bucket e partitioning.
- Introduction to ASP.NET Core Data Protection - Introduzione a Data Protection, key management e scenari d’uso.
- Data Protection consumer APIs - Dettagli pratici su IDataProtectionProvider, IDataProtector e purpose strings.
- Enforce HTTPS in ASP.NET Core - Indicazioni ufficiali su HTTPS redirection e HSTS.
- OWASP File Upload Cheat Sheet - Best practice per validazione, scanning e gestione sicura dei file caricati.
- OWASP Content Security Policy Cheat Sheet - Linee guida pratiche per progettare una CSP efficace.
- OWASP Secrets Management Cheat Sheet - Approccio concreto alla gestione dei secret in applicazioni e infrastrutture.

