CORS: wat is het en waarom blokkeert de browser een API | POLPROG Naar de inhoud

CORS: wat is het en waarom blokkeert de browser een API

CORS is geen mechanisme dat simpelweg verbindingen met een API blokkeert. Het is een browserprotocol bovenop de same-origin policy dat bepaalt of JavaScript van de ene origin een response van een andere origin mag lezen. In sommige gevallen wordt de HTTP-request gewoon verzonden en blokkeert de browser alleen scripttoegang tot de response. In andere gevallen stuurt de browser eerst een OPTIONS-preflight en bepaalt het resultaat of de echte request überhaupt wordt verzonden.

Gepubliceerd Geschreven door Leestijd 10 min lezen

CORS is geen mechanisme dat simpelweg verbindingen met een API blokkeert. Het is een browserprotocol bovenop de same-origin policy dat bepaalt of JavaScript van de ene origin een response van een andere origin mag lezen. In sommige gevallen wordt de HTTP-request gewoon verzonden en blokkeert de browser alleen scripttoegang tot de response. In andere gevallen stuurt de browser eerst een OPTIONS-preflight en bepaalt het resultaat of de echte request überhaupt wordt verzonden.

Op deze pagina
  1. 1CORS begint bij de same-origin policy
  2. 2Wat een origin precies is
  3. 3Wat browser blokkeert API echt betekent
  4. 4Eenvoudige requests: wanneer preflight wordt overgeslagen
  5. 5Preflight OPTIONS: wat de browser controleert
  6. 6De belangrijkste CORS-headers
  7. 7`Access-Control-Allow-Origin`: wildcard of expliciete origin
  8. 8Cookies en credentials: een veelvoorkomende foutbron
  9. 9Waarom `mode: "no-cors"` meestal niets oplost
  10. 10Waarom curl, Postman of een backend wel kunnen werken
  11. 11CORS vervangt CSRF-bescherming of autorisatie niet
  12. 12Veelvoorkomende fouten en debugging
  13. 13Gevaarlijke CORS-configuraties
  14. 14Performance: preflightkosten en `Access-Control-Max-Age`
  15. 15Praktische configuratie en deploymentchecklist

CORS begint bij de same-origin policy

De same-origin policy is een fundamenteel beveiligingsmechanisme van browsers. Ze beperkt hoe een document of script data van een andere origin kan lezen en voorkomt bijvoorbeeld dat een kwaadaardige site gegevens leest uit een dienst waar de gebruiker al is ingelogd. [3]

CORS, Cross-Origin Resource Sharing, is een HTTP-mechanisme waarmee de server deze regel gericht kan versoepelen en expliciet kan aangeven welke origins een response mogen lezen. De Fetch Standard beschrijft CORS als een opt-in protocol. [1][2]

Wat een origin precies is

PaginaDoel-URLRelatieWaarom
https://app.example.comhttps://app.example.com/apiZelfde originZelfde schema, host en poort
https://app.example.comhttps://api.example.comAndere originAndere host
https://app.example.comhttp://app.example.comAndere originAnder schema
https://app.example.comhttps://app.example.com:8443Andere originAndere poort
https://app.example.comhttps://app.example.com/v2Zelfde originAlleen het pad verandert

Een origin wordt bepaald door schema, host en poort. Twee URL's zijn alleen same-origin als alle drie gelijk zijn. Het pad telt niet mee. [3][4]

`https://app.example.com` en `https://api.example.com` zijn dus verschillende origins. Hetzelfde geldt voor HTTP tegenover HTTPS of verschillende poorten. [4]

Same-origin en same-site zijn niet hetzelfde. Dat is vooral belangrijk bij cookies, omdat `SameSite` het site-concept gebruikt en CORS het origin-concept. [13]

Wat browser blokkeert API echt betekent

De uitspraak dat de browser de request blokkeert is vaak te simpel. Bij eenvoudige cross-origin requests kan de browser de request verzenden, een geldige HTTP-response ontvangen en vervolgens weigeren die response aan JavaScript beschikbaar te stellen als de CORS-check faalt. [1][6]

Bij requests met preflight is de flow anders. De browser stuurt eerst `OPTIONS`. Als de preflight faalt, wordt de echte request niet verzonden. [2][5][6]

Dit verschil is belangrijk bij acties die data wijzigen. Een CORS-fout betekent niet automatisch dat de backend helemaal geen request heeft ontvangen.

Eenvoudige requests: wanneer preflight wordt overgeslagen

MethodeVoorbeeldheadersEenvoudige requestEffect
GETAcceptJaGeen preflight
POSTContent-Type: text/plainJaGeen preflight
POSTContent-Type: application/jsonNeeJSON veroorzaakt preflight
PUTContent-Type: application/jsonNeeMethode veroorzaakt preflight
GETAuthorization: Bearer ...NeeHeader veroorzaakt preflight

Een request is alleen eenvoudig als hij aan de CORS-safelist voldoet. Toegestane methoden zijn `GET`, `HEAD` en `POST`. Handmatig ingestelde headers moeten safelisted zijn en `Content-Type` mag alleen `application/x-www-form-urlencoded`, `multipart/form-data` of `text/plain` gebruiken. [1]

Een normale `POST` met `Content-Type: application/json` is niet eenvoudig en veroorzaakt meestal een preflight. Dat geldt ook voor `PUT`, `DELETE` en custom headers zoals `X-Request-ID`. [1][11]

Preflight OPTIONS: wat de browser controleert

Een preflight is een automatische `OPTIONS`-request. De browser stuurt `Origin`, `Access-Control-Request-Method` en indien nodig `Access-Control-Request-Headers`. De server antwoordt met toegestane origins, methoden en headers. [2][5]

Wil de app bijvoorbeeld `DELETE` met `Authorization` verzenden, dan kan de browser eerst controleren of die origin die methode en header mag gebruiken. Pas na toestemming volgt de echte request. [5]

Een preflight voegt een extra round trip toe. Het resultaat kan wel worden opgeslagen in een aparte CORS-preflightcache naast de normale HTTP-cache. [5][10]

De belangrijkste CORS-headers

HeaderRichtingDoel
OriginRequestOrigin die de request initieert
Access-Control-Allow-OriginResponseOrigin waarvan code de response mag ontvangen
Access-Control-Allow-MethodsPreflight-responseMethoden toegestaan door preflight
Access-Control-Allow-HeadersPreflight-responseRequestheaders toegestaan door preflight
Access-Control-Allow-CredentialsResponseToestemming om een credentialed response te delen
Access-Control-Expose-HeadersResponseExtra responseheaders zichtbaar voor JavaScript
Access-Control-Max-AgePreflight-responseHoe lang het preflightresultaat wordt gecachet
Vary: OriginResponseVertelt caches dat de response varieert per Origin

`Origin` is de requestheader die de initiërende origin aangeeft. `Access-Control-Allow-Origin` is een server-responseheader die bepaalt of de response met code van die origin gedeeld mag worden. [1][2][8]

`Access-Control-Allow-Methods` en `Access-Control-Allow-Headers` zijn vooral relevant voor preflight. `Access-Control-Allow-Credentials` staat het delen van een response met credentials toe en `Access-Control-Expose-Headers` kan extra responseheaders zichtbaar maken voor JavaScript. [1][2]

Als de server `Access-Control-Allow-Origin` dynamisch varieert, adviseert MDN ook `Vary: Origin`. [1]

`Access-Control-Allow-Origin`: wildcard of expliciete origin

`Access-Control-Allow-Origin: *` staat toe dat een response met iedere origin wordt gedeeld voor requests zonder credentials. Dat kan correct zijn voor een echt publieke API. [2][8]

Voor private of gevoelige endpoints moet de toegestane set zo klein mogelijk zijn. MDN en OWASP adviseren specifieke origins wanneer wereldwijde toegang niet nodig is. [14][17]

Als meerdere origins zijn toegestaan, moet de server de inkomende `Origin` tegen een allowlist controleren en alleen de goedgekeurde waarde terugsturen. Blind reflecteren is onveilig. [8][16]

Cookies en credentials: een veelvoorkomende foutbron

Fetch kan `credentials: "include"` gebruiken voor credentials in cross-origin requests. Dat alleen is niet genoeg. De server moet `Access-Control-Allow-Credentials: true` en een expliciete `Access-Control-Allow-Origin` teruggeven. `*` is dan niet toegestaan. [2][6][9]

Cookies volgen daarnaast hun eigen regels. `SameSite` kan voorkomen dat een cookie bij een cross-site request wordt verzonden en `SameSite=None` vereist `Secure`. [13]

CORS en `SameSite` lossen verschillende problemen op. CORS kan correct zijn terwijl het cookie ontbreekt, of het cookie kan worden verzonden terwijl CORS JavaScript alsnog de response ontzegt.

Waarom `mode: "no-cors"` meestal niets oplost

`fetch(..., { mode: "no-cors" })` omzeilt CORS niet wanneer de applicatie de API-response moet lezen. Methoden en headers worden beperkt en de response is opaque. JavaScript kan body en headers niet lezen en de zichtbare status is `0`. [6][7][11]

`no-cors` heeft gespecialiseerde toepassingen, onder andere bij sommige Service Worker-scenario's, maar is meestal fout voor een normale JSON-API. [6]

Als een externe API geen CORS aanbiedt, kan een eigen backend of proxy de server-to-server request uitvoeren. [11][12]

Waarom curl, Postman of een backend wel kunnen werken

De same-origin policy en CORS worden door de browser afgedwongen. Een HTTP-client buiten dat model ondergaat dezelfde CORS-check niet. Daardoor kan een endpoint in curl of een API-tool werken terwijl JavaScript in een pagina wordt geblokkeerd. [2][3][16]

Dit laat ook zien waarom CORS geen API-authenticatie is. Een niet-browserclient kan zelf een `Origin` kiezen, daarom waarschuwt OWASP om die header niet als identiteitsbewijs te gebruiken. [16]

De API heeft nog steeds normale autorisatie, tokens, sessies, permissiecontroles en server-side validatie nodig.

CORS vervangt CSRF-bescherming of autorisatie niet

De same-origin policy beperkt vooral cross-origin reads. Formulieren en sommige eenvoudige requests kunnen nog steeds cross-origin worden verzonden. Cookie-gebaseerde applicaties hebben daarom nog steeds CSRF-bescherming nodig. [3][15]

OWASP adviseert expliciet om CORS of `Origin` niet als enige toegangscontrole voor gevoelige resources te gebruiken. Authenticatie en autorisatie blijven noodzakelijk. [16]

CORS bepaalt of een browser een response aan code van een origin mag tonen. Het bepaalt niet of een gebruiker of client de actie mag uitvoeren.

Veelvoorkomende fouten en debugging

Een ontbrekende `Access-Control-Allow-Origin` is een veelvoorkomende fout. Andere oorzaken zijn een niet-toegestane methode, ontbrekende entries in `Access-Control-Allow-Headers`, `*` met credentials, een kapotte OPTIONS-response of redirects in de CORS-flow. [11][12]

JavaScript krijgt bewust weinig details. De browserconsole en het Network-paneel zijn nuttiger voor de echte oorzaak. [1][11]

`CORS request did not succeed` kan ook worden veroorzaakt door DNS, timeout, geweigerde verbinding, TLS, mixed content of extensies. [11]

  • Controleer de exacte frontend-origin: schema, host en poort.
  • Inspecteer `OPTIONS` als die bestaat.
  • Controleer status en `Access-Control-Allow-*` van de preflight.
  • Controleer dat ook de echte response de benodigde CORS-headers bevat.
  • Controleer bij cookies `credentials`, `Access-Control-Allow-Credentials`, `SameSite` en `Secure`.
  • Controleer redirects, TLS, mixed content en netwerkfouten.
  • Vergelijk de browserrequest met een werkende curl- of API-tool-request.

Gevaarlijke CORS-configuraties

Blind de ontvangen `Origin` teruggeven is gevaarlijk wanneer requests met credentials gevoelige data opleveren. Een aanvaller kan een eigen origin beheren en proberen responses te lezen in de context van een ingelogde gebruiker. [16]

Te brede reguliere expressies en vertrouwen in alle subdomeinen zijn ook riskant als een subdomein kan worden overgenomen. OWASP adviseert exacte allowlistmatching. [16][17]

`Access-Control-Allow-Origin: *` is niet automatisch een kwetsbaarheid voor volledig publieke data zonder credentials. Het probleem is een policy die meer toegang geeft dan bedoeld. [8][17]

Performance: preflightkosten en `Access-Control-Max-Age`

Een preflight voegt voor de echte request een extra `OPTIONS` toe en kan latency verhogen. Het resultaat kan met `Access-Control-Max-Age` in een aparte preflightcache worden opgeslagen. [5][10]

MDN noemt 5 seconden als standaard zonder header. Browsers hanteren eigen maxima. Firefox limiteert tot 86400 seconden en Chromium vanaf versie 76 tot 7200 seconden. [10]

Stabiele policies kunnen worden gecachet, maar niet zo lang dat een per ongeluk te brede policy moeilijk snel kan worden ingetrokken.

Praktische configuratie en deploymentchecklist

Bepaal eerst of het endpoint überhaupt cross-origin beschikbaar moet zijn. Zo niet, voeg geen CORS-headers toe. Zo ja, definieer exacte origins, benodigde methoden en headers en of credentials nodig zijn. [14][17]

Bij een dynamische allowlist stuur je alleen een gevalideerde origin terug en voeg je `Vary: Origin` toe. Met credentials zijn een expliciete origin en `Access-Control-Allow-Credentials: true` vereist. Frontendcode kan deze serverbeslissing niet vervangen. [1][2]

Test na deployment een eenvoudige request, preflight, cookie-scenario, niet-toegestane origin en echte fouten in DevTools.

  • Is cross-origin toegang echt nodig?
  • Welke exacte origins moeten worden toegestaan?
  • Zijn credentials nodig?
  • Welke methoden en requestheaders zijn nodig?
  • Bereikt `OPTIONS` de applicatie en antwoordt die correct?
  • Bevat de echte response ook de juiste CORS-headers?
  • Bevatten dynamische responses `Vary: Origin`?
  • Vereist het endpoint nog steeds normale authenticatie en autorisatie?
  • Is de policy getest met zowel een toegestane als niet-toegestane origin?

CORS hoort te worden gezien als browsercontrole over het delen van responses, niet als universele beveiligingsgrens voor een API. Bepaal eerst de origins van frontend en API, controleer daarna of de request eenvoudig is of preflight nodig heeft en inspecteer vervolgens OPTIONS en de CORS-headers op de echte response. Een veilige configuratie staat alleen noodzakelijke origins, methoden en headers toe, terwijl authenticatie en autorisatie in de applicatielaag blijven.

CORS Same-Origin Policy Web Security HTTP API Fetch API Preflight Cookies CSRF Frontend

Veelgestelde vragen

Wat is CORS?

CORS is een HTTP-protocol waarmee browsers het delen van responses tussen verschillende origins controleren. De server verklaart via headers welke origins een response mogen lezen. [1][2]

Waarom werkt de API in Postman maar niet in de browser?

Omdat CORS door browsers wordt afgedwongen als onderdeel van de same-origin policy. HTTP-clients buiten de browser vallen niet onder dezelfde controle. [2][3][16]

Blokkeert CORS altijd het verzenden van de request?

Nee. Een eenvoudige request kan worden verzonden terwijl alleen JavaScripttoegang tot de response wordt geblokkeerd. Als een vereiste preflight faalt, wordt de echte request niet verzonden. [1][5][6]

Wat veroorzaakt een preflight?

Onder andere methoden buiten GET, HEAD, POST, custom headers en Content-Type: application/json. De browser stuurt eerst OPTIONS. [1][5]

Moet Access-Control-Allow-Origin in frontendcode worden toegevoegd?

Nee. Het is een server-responseheader. Frontendcode kan zichzelf deze toestemming niet geven. [1][8][12]

Lost mode: "no-cors" CORS op?

Niet voor een normale API waarvan je data moet lezen. De response wordt opaque en body, headers en normale status zijn niet leesbaar. [6][7]

Mag Access-Control-Allow-Origin: * worden gebruikt?

Ja voor publieke responses zonder credentials. Niet voor credentialed responses. [2][8][9]

Hoe werkt CORS met cookies?

Frontend gebruikt vaak credentials: "include", terwijl de server een expliciete origin en Access-Control-Allow-Credentials: true moet sturen. Het cookie blijft afhankelijk van SameSite en Secure. [6][9][13]

Beschermt CORS een API tegen curl of bots?

Nee. CORS beperkt browser-JavaScript. Een externe client kan een eigen request en Origin sturen, dus autorisatie blijft nodig. [16]

Beschermt CORS tegen CSRF?

Niet volledig. Cookie-gebaseerde applicaties hebben nog steeds CSRF-bescherming nodig. [3][15][16]

Waarom Vary: Origin?

Als Access-Control-Allow-Origin dynamisch varieert, vertelt Vary: Origin caches dat de response afhangt van de aanvragende origin. [1]

Hoe verminder je preflights?

Cache het resultaat met Access-Control-Max-Age of gebruik eenvoudige requests wanneer dat functioneel en qua beveiliging passend is. [1][10][11]

Bronnen en referenties

  1. MDN, Cross-Origin Resource Sharing (CORS)123456789101112131415
  2. WHATWG, Fetch Standard, CORS protocol123456789101112
  3. MDN, Same-origin policy123456
  4. MDN, Origin12
  5. MDN, Preflight request1234567
  6. MDN, Using the Fetch API12345678
  7. MDN, RequestInit12
  8. MDN, Access-Control-Allow-Origin123456
  9. MDN, Access-Control-Allow-Credentials123
  10. MDN, Access-Control-Max-Age1234
  11. MDN, CORS errors1234567
  12. MDN, CORS header Access-Control-Allow-Origin missing123
  13. MDN, Set-Cookie123
  14. MDN, Practical CORS configuration12
  15. MDN, Cross-site request forgery (CSRF)12
  16. OWASP, HTML5 Security Cheat Sheet, Cross Origin Resource Sharing123456789
  17. OWASP, HTTP Headers Cheat Sheet, Access-Control-Allow-Origin1234

Was dit nuttig?

Ontvang nieuwe artikelen per e-mail

Eén korte e-mail per nieuw blogartikel. Geen spam, uitschrijven in één klik.

We gebruiken je e-mail alleen om nieuwe artikelen te sturen. Geen delen met derden.

Terug naar de blog