Prima o poi capita a tutti: il codice JavaScript chiama un'API, la richiesta sembra corretta, ma la console mostra un errore che parla di "CORS policy" e la risposta non arriva. È uno dei messaggi più fraintesi dello sviluppo web, anche perché spesso viene "risolto" in modi che creano problemi di sicurezza. Capire il meccanismo rende la soluzione quasi ovvia.
La same-origin policy
Tutto parte da una regola di sicurezza fondamentale dei browser: la same-origin policy. Uno script caricato da una certa origine può leggere liberamente solo le risposte provenienti dalla stessa origine.
Un'origine è la combinazione di tre elementi: schema, host e porta.
| URL della pagina | URL richiesto | Stessa origine? |
|---|---|---|
https://esempio.it |
https://esempio.it/api |
sì |
https://esempio.it |
http://esempio.it/api |
no (schema) |
https://esempio.it |
https://api.esempio.it |
no (host) |
https://esempio.it |
https://esempio.it:8443 |
no (porta) |
Senza questa regola, qualunque sito visitato potrebbe usare il tuo browser, con i tuoi cookie, per leggere la tua posta o il tuo conto online da un'altra scheda.
Che cos'è CORS
CORS (Cross-Origin Resource Sharing) è il meccanismo con cui un server può dichiarare: "accetto che pagine di queste altre origini leggano le mie risposte". È quindi un modo controllato per allentare la same-origin policy, non una protezione aggiuntiva.
Un punto chiave spesso trascurato: CORS è applicato dal browser. Strumenti come curl o un'applicazione lato server non ne sono soggetti. Se la richiesta funziona da terminale ma non dal browser, il problema è quasi sempre CORS.
Inoltre, in molti casi la richiesta arriva comunque al server e viene eseguita: è il browser che, ricevuta la risposta senza gli header giusti, impedisce allo script di leggerla. Per questo CORS non sostituisce l'autenticazione e la protezione contro richieste indesiderate.
Richieste semplici
Alcune richieste vengono inviate direttamente, con un header Origin che indica da dove provengono:
GET /api/articoli HTTP/1.1
Host: api.esempio.it
Origin: https://www.esempio.it
Il server, se vuole autorizzare quella origine, risponde con:
Access-Control-Allow-Origin: https://www.esempio.it
Se l'header manca o indica un'altra origine, il browser blocca la lettura della risposta. Sono "semplici" le richieste GET, HEAD e POST con header standard e con un Content-Type tra text/plain, application/x-www-form-urlencoded e multipart/form-data.
Le richieste di preflight
Una richiesta con metodo PUT o DELETE, con un header personalizzato come Authorization, o con Content-Type: application/json non è "semplice". Prima di inviarla, il browser chiede il permesso con una richiesta OPTIONS, detta preflight:
OPTIONS /api/articoli/42 HTTP/1.1
Origin: https://www.esempio.it
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization
Il server deve rispondere indicando cosa consente:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.esempio.it
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Solo se la risposta è soddisfacente il browser invia la richiesta vera. Access-Control-Max-Age permette di memorizzare il permesso per un certo numero di secondi, evitando un preflight a ogni chiamata.
Un errore tipico lato server è gestire la rotta solo per i metodi previsti e restituire 404 o 405 alle richieste OPTIONS, oppure richiedere l'autenticazione anche sul preflight: il browser non invia credenziali nel preflight, quindi la richiesta fallisce.
Credenziali e cookie
Per impostazione predefinita, le richieste cross-origin effettuate con fetch non includono i cookie. Per includerli:
fetch('https://api.esempio.it/profilo', {
credentials: 'include'
});
E il server deve rispondere con:
Access-Control-Allow-Origin: https://www.esempio.it
Access-Control-Allow-Credentials: true
In questo caso il valore jolly * non è ammesso per l'origine: il server deve indicare esplicitamente quella consentita. È una regola voluta, per evitare che chiunque possa leggere risposte autenticate.
Configurare CORS in sicurezza
L'errore più comune, e più pericoloso, è riflettere qualsiasi origine ricevuta:
// Da evitare: equivale ad autorizzare tutti
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
Con le credenziali attive, questa configurazione permette a qualsiasi sito di leggere i dati privati degli utenti autenticati. L'approccio corretto è un elenco di origini consentite:
const consentite = new Set([
'https://www.esempio.it',
'https://admin.esempio.it'
]);
const origine = req.headers.origin;
if (consentite.has(origine)) {
res.setHeader('Access-Control-Allow-Origin', origine);
res.setHeader('Vary', 'Origin');
}
L'header Vary: Origin è importante quando la risposta cambia in base all'origine: dice alle cache di non servire a un'origine la risposta preparata per un'altra.
Qualche altra regola:
- usa
*solo per risorse davvero pubbliche e senza credenziali, come un'API di dati aperti o dei font; - non confrontare le origini con controlli parziali come "contiene esempio.it":
esempio.it.sito-malevolo.comlo contiene; - autorizza solo i metodi e gli header che servono.
Alternative a CORS
Se frontend e API sono tuoi, spesso la soluzione più semplice è eliminare la necessità di CORS: servi l'API sullo stesso dominio del sito, per esempio sotto /api, tramite un proxy o le regole di rewrite dell'hosting. Le richieste diventano same-origin e il problema scompare.
In sintesi
- La same-origin policy impedisce agli script di leggere risposte da altre origini (schema, host, porta).
- CORS è il modo in cui il server allenta questa regola, ed è applicato dal browser.
- Le richieste non semplici richiedono un preflight
OPTIONSche il server deve gestire senza autenticazione. - Con le credenziali non si può usare
*: serve un elenco esplicito di origini. - Non riflettere mai l'origine ricevuta senza verificarla e aggiungi
Vary: Origin.