Queste sono le note della lezione del corso dedicata a CORS. Ho scelto CORS come argomento perché in anni di sviluppo web ho visto questo errore mettere in difficoltà chiunque, dai ragazzi al primo stage ad alcuni “senior” con dieci anni di esperienza. Di solito si risolve copiando qualche header trovato su Stack Overflow, l’errore sparisce e nessuno si fa più domande fino alla volta successiva.
In questa lezione voglio fare un passo indietro e raccontare il modello di sicurezza che sta dietro a quell’errore. Quando il perché è chiaro, il come diventa quasi banale.

Il problema
Partiamo da un esempio concreto. Stiamo sviluppando il sito di una pizzeria: il frontend è servito da https://awesomepizza.com e chiama le API per caricare il menu.
fetch(`https://awesomepizza.com/api/pizzas`)
.then(response => response.json())
.then(data => console.log(data.pizzas));
Il server risponde con la lista delle pizze:
HTTP/1.1 200 OK
Server: Apache
Date: Mon, 02 Jul 2023 10:00:00 GMT
Content-Type: application/json
{
"pizzas": [
{
"name": "Margherita",
"description": "Tomato, mozzarella, basil"
},
{
"name": "Marinara",
"description": "Tomato, garlic, oregano"
}
]
}
Fin qui tutto bene. A un certo punto però decidiamo di spostare le API su un sottodominio dedicato, una cosa normalissima quando un progetto cresce.
fetch(`https://api.awesomepizza.com/pizzas`)
.then(response => response.json())
.then(data => console.log(data.pizzas));
E la chiamata, che fino a un minuto fa funzionava, adesso fallisce con questo errore:
Access to fetch at ‘https://api.awesomepizza.com/pizzas’ from origin ‘https://awesomepizza.com’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
Il codice è lo stesso e il server risponde come prima. Abbiamo solo cambiato il dominio di destinazione. Cosa è successo?
Chi sta bloccando cosa
La prima cosa da capire, e anche quella che all’inizio confonde di più, è che il server in tutto questo è innocente. La richiesta parte, arriva a destinazione, il server elabora la risposta e la rimanda indietro come ha sempre fatto. È il browser che, una volta ricevuta la risposta, decide di non consegnarla al nostro JavaScript. Questo vale per le richieste semplici come la nostra GET; per altre richieste il browser arriva a bloccare ancora prima, senza nemmeno inviarle, ma ci torniamo più avanti quando parleremo di preflight.
Quando un sito prova a leggere dati da un’altra origine, il browser blocca la risposta. È questo blocco a generare l’errore CORS.
Visto da fuori sembra un comportamento assurdo, e infatti la prima reazione di chiunque è cercare il modo di spegnerlo. Per capire perché invece ha senso, e perché conviene lasciarlo acceso, dobbiamo parlare di Same-Origin Policy.
La Same-Origin Policy
La Same-Origin Policy è una delle misure di sicurezza più vecchie del web. La introdusse Netscape nel 1995 insieme a JavaScript, perché si era capito subito che un linguaggio eseguito dentro il browser apriva possibilità di attacco che prima semplicemente non esistevano.
Lo scenario classico, quello per cui la protezione è nata, è questo: un utente sta navigando su https://evil-site.com, un sito che contiene JavaScript malevolo, e in un’altra tab è autenticato sul sito della sua banca, https://mybank.com. Per molti anni i browser hanno inviato i cookie di sessione a ogni richiesta verso il dominio della banca, da qualunque pagina partisse. Il sito malevolo poteva quindi fare una fetch verso https://mybank.com/api/account-balance e leggersi il saldo del conto, perché la sua richiesta viaggiava con i cookie validi dell’utente. La Same-Origin Policy blocca la lettura della risposta e chiude il problema alla radice.
Lo scenario della banca oggi è mitigato anche per altre strade, perché i cookie hanno SameSite=Lax come default e fetch non li invia verso altre origini se non glielo si chiede con credentials: 'include'. Sarebbe però un errore pensare che il rischio appartenga al passato. Basta spostare l’attacco dalla banca alla rete locale.
Mentre siamo su https://evil-site.com, il JavaScript della pagina può fare una fetch verso http://localhost:3000, dove magari gira il nostro server di sviluppo, oppure verso il pannello di amministrazione del router di casa. Questi servizi spesso non chiedono alcuna credenziale: si fidano del fatto di essere raggiungibili solo dall’interno della rete. Il nostro browser però sta dentro quella rete, e senza Same-Origin Policy qualunque sito potrebbe usarlo come ponte per leggere quello che quei servizi espongono. È anche il motivo per cui i browser stanno aggiungendo protezioni ulteriori proprio su questo fronte, come la proposta Private Network Access.
In entrambi i casi il principio è lo stesso: il browser blocca la lettura delle risposte cross-origin, così un sito non può leggere dati che appartengono a un altro contesto.
Cosa significa stessa origine
Una origine è composta da tre elementi: schema (il protocollo), host e porta. Per chi vuole la definizione formale, si trova nella RFC 6454, “The Web Origin Concept”.
https://awesomepizza.com -> https://awesomepizza.com OK (stessa origine)
https://awesomepizza.com -> https://api.awesomepizza.com NO (host diversi)
https://awesomepizza.com -> http://awesomepizza.com NO (protocolli diversi)
https://awesomepizza.com -> https://awesomepizza.com:84 NO (porte diverse)
Basta che uno solo dei tre elementi sia diverso perché le due origini siano considerate diverse. Ed ecco spiegato il nostro esempio: awesomepizza.com e api.awesomepizza.com sono host diversi, quindi origini diverse, quindi il browser blocca.
A questo punto però sorgono domande legittime. Se il browser blocca tutto, come fa un frontend a parlare con un backend su un altro dominio? Come si integrano le API di terze parti? Come fanno a funzionare Google Fonts o i CDN?
CORS è la soluzione
CORS sta per Cross-Origin Resource Sharing ed è il meccanismo con cui si può allentare la Same-Origin Policy in modo controllato. Oggi è specificato nel Fetch Standard del WHATWG, lo stesso documento che definisce il comportamento di fetch.
L’idea è che il server può dichiarare, tramite header HTTP, da quali origini accetta di farsi leggere. Il browser legge questa dichiarazione e la fa rispettare.
Vale la pena ripeterlo: la Same-Origin Policy protegge gli utenti, non i server. Una chiamata bloccata dal browser si può fare tranquillamente con curl o Postman. In quel caso però la richiesta la state facendo voi, consapevolmente, e non un sito malevolo che sfrutta una vostra sessione attiva.
Come funziona in pratica
Il meccanismo si regge su una coppia di header. Il browser aggiunge Origin a ogni richiesta cross-origin, e il server risponde con Access-Control-Allow-Origin per dire chi è autorizzato a leggere.
Riprendiamo la pizzeria. Quando il browser fa la chiamata verso il sottodominio, invia questo:
GET https://api.awesomepizza.com/pizzas HTTP/1.1
Origin: https://awesomepizza.com
Il server, se vuole che la risposta sia leggibile da quell’origine, deve includere l’header di autorizzazione. Con Express è una riga:
// Nel server (esempio con Express.js)
app.get('/pizzas', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://awesomepizza.com');
res.json({ pizzas: [...] });
});
Il browser confronta i due valori. Se l’origine della pagina corrisponde a quella dichiarata in Access-Control-Allow-Origin, consegna la risposta al codice JavaScript, altrimenti la blocca e otteniamo il famoso errore.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://awesomepizza.com
Server: Apache
Date: Mon, 02 Jul 2023 10:00:00 GMT
Content-Type: application/json
{ "pizzas": [{...}, {...}, {...}]}
Con questa configurazione solo https://awesomepizza.com può leggere i dati. Se qualcuno provasse la stessa fetch da evilpizza.com, il browser la bloccherebbe.
Per le API pubbliche esiste la wildcard, che autorizza qualunque origine:
Access-Control-Allow-Origin: *
Se invece vogliamo autorizzare più origini specifiche, ad esempio il sito di produzione e quello di staging, serve un minimo di logica lato server: si confronta l’header Origin in arrivo con una whitelist e, quando c’è corrispondenza, si riflette quell’origine nella risposta. In questo caso conviene aggiungere anche Vary: Origin, così le cache intermedie non rischiano di servire a un’origine la risposta preparata per un’altra.
Altri header utili
Access-Control-Allow-Origin è l’header principale, ma ne esistono altri per controlli più fini:
Access-Control-Allow-Methods: quali metodi HTTP sono permessi (GET, POST, PUT, DELETE)Access-Control-Allow-Headers: quali header possono essere inviati nella richiestaAccess-Control-Allow-Credentials: autorizza il browser a consegnare la risposta quando la richiesta includeva le credenziali. L’invio dei cookie, a essere precisi, lo decide il client concredentials: 'include'; questo header dice al browser che il server è d’accordo. Se è true non si può usare la wildcard.
Le richieste preflight
C’è un ultimo pezzo del meccanismo che vale la pena conoscere, perché prima o poi ve lo ritroverete davanti nel Network tab.
Per alcune richieste il browser, prima di inviare la richiesta vera e propria, fa una chiamata OPTIONS al server per chiedere il permesso. Si chiama preflight request e succede con le richieste che la specifica considera “non semplici”: quelle che usano metodi diversi da GET, HEAD e POST, che includono header personalizzati, o il cui Content-Type non rientra tra application/x-www-form-urlencoded, multipart/form-data e text/plain. Una POST con Content-Type: application/json, che è il caso più comune quando si lavora con le API, ricade quindi nella categoria e viene sempre preceduta da una preflight.
// Prima il browser chiede il permesso, dichiarando
// metodo e header che intende usare
OPTIONS /pizzas HTTP/1.1
Origin: https://awesomepizza.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
// Il server autorizza
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://awesomepizza.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: content-type
// Solo dopo parte la richiesta vera
POST /pizzas HTTP/1.1
Origin: https://awesomepizza.com
Content-Type: application/json
{ "name": "Quattro Stagioni", "price": 12 }
I framework moderni gestiscono le preflight in automatico, quindi nella pratica raramente bisogna metterci mano. Sapere che esistono però evita di perdere tempo a chiedersi perché nel Network tab compaiono due richieste al posto di una.
Errori e fraintendimenti comuni
Chiudo con i dubbi e le obiezioni che sento più spesso, a lezione e in ufficio.
«Da Postman funziona, quindi il problema è il frontend»
Postman non applica la Same-Origin Policy perché non è un browser. Se la chiamata funziona da Postman e fallisce dal browser, al server mancano gli header CORS. Il frontend non ha niente da sistemare.
«Non si può disattivare CORS?»
In un certo senso CORS è già disattivato: senza header il browser blocca tutto. Quello che si vuole ottenere è il contrario, cioè abilitarlo con gli header corretti. E no, non è un bug da aggirare, è una misura di sicurezza che lavora per noi.
«Funziona in locale ma non in produzione»
In locale capita spesso che frontend e backend girino sulla stessa origine, stesso host e stessa porta, quindi la Same-Origin Policy non entra mai in gioco. In produzione stanno su domini diversi e il problema emerge.
«Metto Access-Control-Allow-Origin a asterisco e risolvo»
Risolve l’errore, questo sì. Ma autorizza qualunque sito ad accedere alle API. Per un’API davvero pubblica va benissimo, per un’API che gestisce dati degli utenti è una porta lasciata aperta.
«Il mio backend non ha CORS configurato»
Ogni framework ha la sua soluzione pronta: Express ha il pacchetto cors, Django ha django-cors-headers, Laravel ha un middleware integrato. In tutti i casi si tratta di poche righe di configurazione.
«Perché compaiono due richieste nel Network tab?»
È la preflight di cui parlavamo sopra: prima la OPTIONS con cui il browser chiede il permesso, poi la richiesta vera.
In sintesi
Se di questa lezione dovessero restarvi solo quattro cose, vorrei che fossero queste:
- la Same-Origin Policy protegge gli utenti, bloccando la lettura delle risposte cross-origin
- CORS serve ad autorizzare origini specifiche a leggere le risorse
- la configurazione sta sul server, mai sul client
- le preflight sono normali per le richieste non semplici
La prossima volta che vi compare un errore CORS, ricordate che il browser vi sta solo dicendo che il server non ha autorizzato la vostra origine a leggere le sue risposte. Si sistema da lì, con gli header giusti.
Per approfondire
- Same-origin policy su MDN
- CORS su MDN
- RFC 6454 - The Web Origin Concept
- Fetch Standard - CORS protocol
- CORS tester - per testare le configurazioni CORS