CORS parte dalla same-origin policy
La same-origin policy è un meccanismo di sicurezza fondamentale del browser. Limita la capacità di un documento o script di leggere dati provenienti da un altro origin e impedisce, tra le altre cose, che un sito malevolo legga dati da un servizio in cui l'utente è autenticato. [3]
CORS, Cross-Origin Resource Sharing, è un meccanismo HTTP che permette al server di rilassare questa regola e dichiarare esplicitamente quali origins possono leggere una risposta. Il Fetch Standard lo descrive come protocollo opt-in. [1][2]
Che cosa significa esattamente origin
| Pagina | URL target | Relazione | Perché |
|---|---|---|---|
| https://app.example.com | https://app.example.com/api | Stesso origin | Stesso schema, host e porta |
| https://app.example.com | https://api.example.com | Origin diverso | Host diverso |
| https://app.example.com | http://app.example.com | Origin diverso | Schema diverso |
| https://app.example.com | https://app.example.com:8443 | Origin diverso | Porta diversa |
| https://app.example.com | https://app.example.com/v2 | Stesso origin | Cambia solo il path |
Un origin è definito da schema, host e porta. Due URL sono same-origin solo se tutti e tre coincidono. Il path non conta. [3][4]
`https://app.example.com` e `https://api.example.com` sono quindi origins diversi. Lo stesso vale per HTTP contro HTTPS o porte differenti. [4]
Same-origin e same-site non sono equivalenti. È importante soprattutto con i cookie, perché `SameSite` usa il concetto di site mentre CORS usa origin. [13]
Cosa significa davvero quando il browser blocca l'API
Dire che il browser blocca la richiesta è spesso troppo generico. Per richieste cross-origin semplici può inviare la richiesta, ricevere una risposta HTTP valida e poi rifiutare di esporla a JavaScript se il controllo CORS fallisce. [1][6]
Per richieste con preflight il flusso è diverso. Il browser invia prima `OPTIONS`. Se il preflight fallisce, la richiesta reale non viene inviata. [2][5][6]
Questa differenza è importante con operazioni che modificano dati. Un errore CORS non significa automaticamente che il backend non abbia ricevuto alcuna richiesta.
Richieste semplici: quando il browser evita il preflight
| Metodo | Headers di esempio | Richiesta semplice | Effetto |
|---|---|---|---|
| GET | Accept | Sì | Nessun preflight |
| POST | Content-Type: text/plain | Sì | Nessun preflight |
| POST | Content-Type: application/json | No | JSON causa preflight |
| PUT | Content-Type: application/json | No | Il metodo causa preflight |
| GET | Authorization: Bearer ... | No | L'header causa preflight |
Una richiesta è semplice solo se rispetta la CORS safelist. I metodi consentiti sono `GET`, `HEAD` e `POST`. Gli headers impostati manualmente devono essere safelisted e `Content-Type` può usare solo `application/x-www-form-urlencoded`, `multipart/form-data` o `text/plain`. [1]
Un normale `POST` con `Content-Type: application/json` non è semplice e in genere causa preflight. Lo stesso vale per `PUT`, `DELETE` o headers personalizzati come `X-Request-ID`. [1][11]
Preflight OPTIONS: cosa controlla il browser
Il preflight è una richiesta `OPTIONS` automatica. Il browser invia `Origin`, `Access-Control-Request-Method` e, se necessario, `Access-Control-Request-Headers`. Il server risponde con origins, metodi e headers consentiti. [2][5]
Se l'app vuole inviare `DELETE` con `Authorization`, il browser può prima verificare se quell'origin può usare metodo e header. Solo una risposta positiva permette la richiesta reale. [5]
Il preflight aggiunge un round trip, ma il risultato può essere memorizzato in una cache CORS dedicata, separata dalla cache HTTP normale. [5][10]
Gli headers CORS più importanti
| Header | Direzione | Ruolo |
|---|---|---|
| Origin | Richiesta | Origin che avvia la richiesta |
| Access-Control-Allow-Origin | Risposta | Origin il cui codice può ricevere la risposta |
| Access-Control-Allow-Methods | Risposta preflight | Metodi consentiti dal preflight |
| Access-Control-Allow-Headers | Risposta preflight | Request headers consentiti dal preflight |
| Access-Control-Allow-Credentials | Risposta | Permesso di esporre una risposta con credentials |
| Access-Control-Expose-Headers | Risposta | Response headers aggiuntivi visibili a JavaScript |
| Access-Control-Max-Age | Risposta preflight | Durata cache del risultato preflight |
| Vary: Origin | Risposta | Indica alla cache che la risposta varia per Origin |
`Origin` identifica l'origin che avvia la richiesta. `Access-Control-Allow-Origin` è un header di risposta che dice al browser se la risposta può essere condivisa con codice di quell'origin. [1][2][8]
`Access-Control-Allow-Methods` e `Access-Control-Allow-Headers` sono particolarmente importanti nel preflight. `Access-Control-Allow-Credentials` permette di esporre una risposta con credentials, mentre `Access-Control-Expose-Headers` rende disponibili headers aggiuntivi a JavaScript. [1][2]
Se il server restituisce dinamicamente `Access-Control-Allow-Origin`, MDN raccomanda anche `Vary: Origin`. [1]
`Access-Control-Allow-Origin`: wildcard o origin esplicito
`Access-Control-Allow-Origin: *` permette di condividere una risposta con qualsiasi origin per richieste senza credentials. Può essere corretto per un'API realmente pubblica. [2][8]
Per endpoints privati o sensibili l'insieme consentito dovrebbe essere minimo. MDN e OWASP raccomandano origins specifici quando non serve accesso globale. [14][17]
Con più origins consentiti, il server deve confrontare l'`Origin` con una allowlist e restituire solo quello validato. Non deve riflettere ciecamente qualsiasi origin. [8][16]
Cookie e credentials: fonte comune di problemi
Fetch può usare `credentials: "include"` per includere credentials nelle richieste cross-origin. Non basta. Il server deve restituire `Access-Control-Allow-Credentials: true` e un `Access-Control-Allow-Origin` esplicito. `*` non è ammesso in questo caso. [2][6][9]
I cookie seguono inoltre regole proprie. `SameSite` può impedire l'invio del cookie in una richiesta cross-site e `SameSite=None` richiede `Secure`. [13]
CORS e `SameSite` risolvono problemi diversi. CORS può essere corretto ma il cookie non essere inviato, oppure il cookie può essere inviato e CORS continuare a bloccare JavaScript.
Perché `mode: "no-cors"` di solito non risolve nulla
`fetch(..., { mode: "no-cors" })` non aggira CORS quando l'applicazione deve leggere la risposta. Metodi e headers sono limitati e la risposta è opaque. JavaScript non può leggere body o headers e lo status esposto è `0`. [6][7][11]
`no-cors` ha usi specializzati, inclusi alcuni casi con Service Worker, ma normalmente è sbagliato per una classica API JSON. [6]
Se non controlli l'API esterna e non offre CORS, un backend o proxy sotto il tuo controllo può fare la richiesta server-to-server. [11][12]
Perché curl, Postman o backend possono funzionare
Same-origin policy e CORS sono applicati dal browser. Un client HTTP fuori da quel modello non è soggetto allo stesso controllo. Per questo un endpoint può funzionare con curl o un tool API ma fallire da JavaScript in pagina. [2][3][16]
Questo dimostra anche che CORS non è autenticazione API. Un client non-browser può scegliere il proprio `Origin`, quindi OWASP sconsiglia di usarlo come prova di identità. [16]
L'API necessita comunque di autorizzazione, token, sessioni, controlli dei permessi e validazione server-side.
CORS non sostituisce CSRF o autorizzazione
La same-origin policy limita soprattutto le letture cross-origin. Form e alcune richieste semplici possono comunque essere inviate tra siti, quindi le applicazioni basate su cookie necessitano ancora di protezioni CSRF. [3][15]
OWASP raccomanda esplicitamente di non usare solo CORS o `Origin` come controllo di accesso a risorse sensibili. Autenticazione e autorizzazione restano necessarie. [16]
CORS risponde alla domanda se il browser può esporre una risposta al codice di un origin, non se l'utente o client ha diritto di eseguire l'operazione.
Errori comuni e diagnosi
La mancanza di `Access-Control-Allow-Origin` è un errore frequente. Altre cause sono metodo non consentito, headers mancanti in `Access-Control-Allow-Headers`, `*` con credentials, risposta OPTIONS errata o redirect nel flusso CORS. [11][12]
JavaScript riceve volutamente pochi dettagli. Console e pannello Network di DevTools sono più utili. [1][11]
`CORS request did not succeed` può anche derivare da DNS, timeout, connessione rifiutata, TLS, mixed content o estensioni. [11]
- Controlla l'origin esatto del frontend: schema, host e porta.
- Ispeziona `OPTIONS` quando presente.
- Verifica status e `Access-Control-Allow-*` del preflight.
- Controlla che anche la risposta reale includa gli headers CORS richiesti.
- Con cookie verifica `credentials`, `Access-Control-Allow-Credentials`, `SameSite` e `Secure`.
- Controlla redirect, TLS, mixed content ed errori di rete.
- Confronta la richiesta del browser con una richiesta funzionante da curl o tool API.
Configurazioni CORS pericolose
Riflettere senza validazione l'`Origin` ricevuto è pericoloso quando richieste con credentials restituiscono dati sensibili. Un attaccante può controllare un origin e tentare di leggere risposte nel contesto di un utente autenticato. [16]
Regex troppo larghe e fiducia in tutti i sottodomini sono rischiose se uno di essi può essere compromesso. OWASP raccomanda una allowlist con confronto preciso. [16][17]
`Access-Control-Allow-Origin: *` non è automaticamente una vulnerabilità per dati interamente pubblici senza credentials. Il problema è una policy più permissiva del necessario. [8][17]
Prestazioni: costo del preflight e `Access-Control-Max-Age`
Il preflight aggiunge una richiesta `OPTIONS` prima di quella reale e può aumentare la latenza. Il risultato può essere memorizzato con `Access-Control-Max-Age` in una cache separata da quella HTTP. [5][10]
MDN indica un valore predefinito di 5 secondi senza header. I browser applicano limiti propri. Firefox limita a 86400 secondi e Chromium dalla versione 76 a 7200 secondi. [10]
Conviene cacheare policy stabili, ma non tanto da rendere difficile revocare rapidamente una regola troppo ampia.
Configurazione pratica e checklist di deploy
Prima decidi se l'endpoint deve davvero essere accessibile cross-origin. Se no, non aggiungere headers CORS. Se sì, definisci allowlist di origins, metodi, headers e necessità di credentials. [14][17]
Per una allowlist dinamica restituisci solo un origin verificato e aggiungi `Vary: Origin`. Con credentials usa origin esplicito e `Access-Control-Allow-Credentials: true`. Il frontend non può sostituire questa decisione del server. [1][2]
Dopo il deploy prova una richiesta semplice, un preflight, un caso con cookie, un origin non consentito e gli errori reali in DevTools.
- L'accesso cross-origin è davvero necessario?
- Quali origins esatti devono essere consentiti?
- Servono credentials?
- Quali metodi e request headers sono necessari?
- `OPTIONS` raggiunge l'applicazione e risponde correttamente?
- La risposta reale contiene gli headers CORS necessari?
- Le risposte dinamiche includono `Vary: Origin`?
- L'endpoint richiede ancora autenticazione e autorizzazione normali?
- La policy è stata testata con origin consentito e non consentito?

