⌨️ Sviluppo web

CORS spiegato: perché il browser blocca la tua richiesta

Same-origin policy, richieste semplici e preflight, header Access-Control e credenziali: come funziona CORS e come configurarlo in modo sicuro sul server.

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.

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.com lo 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 OPTIONS che 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.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche