CORS wird häufig missverstanden — viele Entwickler:innen halten es für einen Sicherheits-Mechanismus, der vor Cross-Origin-Angriffen schützt. Tatsächlich ist es das Gegenteil: CORS ist eine Lockerung der Same-Origin-Policy, die kontrolliert erlaubt, dass eine fremde Origin mit deinem Server spricht. Die Default-Policy ist „nein". CORS sagt „okay, hier sind die Bedingungen". Dieser Artikel zeigt das Modell, die häufigsten Konfigurations-Fehler und die kritische Klarstellung: CORS schützt nicht vor CSRF.
Was die Same-Origin-Policy macht
Eine Origin ist die Kombination <scheme>://<host>:<port>. https://app.example.com:443 und https://api.example.com:443 sind verschiedene Origins.
Die Same-Origin-Policy (SOP) sagt: Skripte einer Origin dürfen Responses anderer Origins nicht lesen. Browser blockt den Read.
Wichtig — was SOP NICHT verhindert:
- Skripte können Requests an fremde Origins schicken (z. B.
<img src="https://other.com/x.png">,fetch('https://other.com/api')— der Request geht raus). - Der Browser blockt nur das Lesen der Response, wenn die Origin nicht zustimmt.
Was SOP verhindert:
- Skript auf
evil.comkann nichtfetch('https://bank.com/balance')machen und die Antwort lesen — auch wenn der User auf bank.com eingeloggt ist und Cookies mitgehen würden.
CORS ist der Mechanismus, mit dem bank.com sagen kann: „okay, partner.com darf die Response lesen."
Simple Requests vs. Preflighted Requests
Simple Requests (klassische HTML-Form-Requests):
- Methode:
GET,HEAD,POST. - Header: nur Standard-Sets (
Accept,Accept-Language,Content-Language,Content-Typemitapplication/x-www-form-urlencoded,multipart/form-data,text/plain). - Kein Custom-Header.
Browser sendet den Request direkt. Server antwortet mit Access-Control-Allow-Origin — wenn ja, kann das Skript die Response lesen.
Preflighted Requests (alles andere — JSON-POSTs, PUT/PATCH/DELETE, Custom-Headers wie Authorization):
Browser sendet zuerst eine OPTIONS-Anfrage (Preflight):
# Preflight-Request vom Browser
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
# Preflight-Response vom Server
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 7200Wenn die Preflight-Response die geplante Aktion erlaubt, schickt der Browser den eigentlichen Request. Sonst wird der echte Request gar nicht gesendet.
Access-Control-Max-Age cached die Preflight-Antwort beim Browser für N Sekunden — spart Roundtrips bei mehreren Requests.
credentials — Cookies und Auth-Header
Default: Browser sendet bei Cross-Origin-Requests keine Cookies und kein Authorization-Header.
Mit Credentials: Wenn der Client explizit credentials: 'include' (fetch) oder withCredentials: true (XHR) setzt, versucht der Browser, Cookies/Auth mitzusenden. Server muss explizit zustimmen:
// Client (auf https://app.example.com)
fetch('https://api.example.com/me', {
credentials: 'include',
});# Server-Response
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueZwei kritische Regeln bei Credentials:
-
Access-Control-Allow-Origin: *ist verboten, wennAccess-Control-Allow-Credentials: truegesetzt ist. Browser lehnt die Response ab. Muss eine konkrete Origin sein. -
Origin-Echo gefährlich: Server-Pattern „nimm
Origin-Header aus dem Request, gib es alsAllow-Originzurück" plusAllow-Credentials: trueist faktisch das gleiche wie Wildcard — jede Origin wird erlaubt. Schwere Klasse. Pattern wird in Bug-Bounty-Reports regelmäßig gefunden.
Typische Konfigurations-Fehler
Fehler 1: Wildcard + Credentials per Origin-Echo
// Schadhaft
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});Effekt: jede Origin darf cross-origin mit Credentials zugreifen. Wenn der User auf einer fremden Site ist, kann diese Site authentifizierte Requests gegen die API machen und die Response lesen. Komplette CORS-Aushebelung.
Schutz: Explizite Allow-Liste.
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com',
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // wichtig für Caching
}
next();
});Fehler 2: Vary-Header vergessen
Wenn Access-Control-Allow-Origin pro Request unterschiedlich gesetzt wird, muss Vary: Origin mit — sonst kann ein CDN/Proxy die Response mit Origin-A für einen Request von Origin-B servieren. Cross-Origin-Leak.
Fehler 3: Subdomain-Wildcard via Regex
// Schadhaft — Regex matched zu viel
if (/example\.com$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// origin = "https://evil-example.com" → matched!
// origin = "https://app.example.com.evil.com" → matched!Korrekt: strikte URL-Match oder strenges Regex:
// Strikt: nur app, admin, www
if (/^https:\/\/(app|admin|www)\.example\.com$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}Fehler 4: null-Origin akzeptieren
Browser sendet Origin: null für Sandboxed iframes, file://-URLs, redirects. Wer das akzeptiert, lässt evil-Iframes auf die API zugreifen.
// Schadhaft
if (origin === 'null' || ALLOWED_ORIGINS.has(origin)) { ... }Niemals null akzeptieren.
CORS ist KEIN CSRF-Schutz
Der wahrscheinlich verbreitetste CORS-Mythos: „Wir haben strikte CORS-Policy, also sind wir CSRF-sicher."
Warum das falsch ist:
CORS kontrolliert, ob Skripte die Response einer Cross-Origin-Request lesen dürfen. Es kontrolliert nicht, ob der Request gesendet wird.
Klassischer CSRF-Angriff (state-changing POST):
<!-- Auf https://evil.com -->
<form action="https://bank.example.com/transfer" method="POST">
<input name="to" value="attacker-account">
<input name="amount" value="1000">
</form>
<script>document.forms[0].submit();</script>Browser submittet das Form (klassischer Form-Submit, keine CORS-Restriction). Cookies gehen mit. Server führt die Aktion aus. Angreifer sieht die Response zwar nicht (CORS blockt das Lesen), aber die Aktion ist passiert.
Konsequenz: CSRF-Schutz braucht eine separate Schicht — siehe Kap 12 CSRF:
- SameSite-Cookies (
LaxoderStrict). - CSRF-Token.
- Origin-/Sec-Fetch-Header-Check server-seitig.
Was CORS Indirekt für CSRF tut:
Bei Preflighted Requests (mit JSON-Body, Custom-Headers) muss der Browser vor dem echten Request einen OPTIONS-Preflight machen. Wenn der Server diesen ablehnt, geht der echte Request gar nicht raus. Das macht JSON-POST-CSRF schwerer — aber:
- Klassischer Form-POST mit
Content-Type: application/x-www-form-urlencodedist Simple Request — kein Preflight. CORS hilft nicht. - Wenn der Server bei Preflight grundsätzlich erlaubt (Wildcard, Origin-Echo), greift CORS-„Schutz" gar nicht.
Pragmatisch: CORS ist Lockerung für legitime Cross-Origin-Use-Cases, kein CSRF-Schutz.
Standard-Library-Patterns
Express (cors-Package):
import cors from 'cors';
app.use(cors({
origin: (origin, callback) => {
const allowed = ['https://app.example.com', 'https://admin.example.com'];
if (!origin || allowed.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 7200,
}));Python (FastAPI):
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=['https://app.example.com', 'https://admin.example.com'],
allow_credentials=True,
allow_methods=['GET', 'POST', 'PUT', 'DELETE'],
allow_headers=['Content-Type', 'Authorization'],
max_age=7200,
)Spring (Java):
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(7200);
}
}CORS für Public APIs
Wenn die API öffentlich sein soll (z. B. Public-Data-API ohne Auth), kann Wildcard sinnvoll sein:
Access-Control-Allow-Origin: *
# KEIN Allow-Credentials → kein Cookie wird gesendetSicher wenn:
- Keine Auth-Cookies mitgesendet werden (Public-Data).
- Read-only oder mit eigenständigen API-Keys/Tokens, nicht via Cookies.
- Keine sensitive Information in Cookies, die nicht für die Ziel-Origin gedacht ist.
Beispiele: öffentliche REST-APIs (Wettervorhersage, OpenStreetMap, GitHub-Public-API-Endpoints).
Test-Strategien
Manuell:
# Preflight simulieren
curl -X OPTIONS https://api.example.com/users \
-H "Origin: https://evil.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization" \
-i
# Schauen: enthält Response Access-Control-Allow-Origin mit evil.com?
# → Origin-Echo-BugBrowser: DevTools Network-Tab zeigt CORS-Fehler im Console.
Automatisiert:
- CORScanner — Standard-Pentest-Tool für CORS-Misconfigs.
- Burp Suite Pro Active Scanner hat CORS-Checks eingebaut.
- OWASP ZAP mit CORS-Plugin.
Integration-Tests:
test('CORS allows app.example.com', async () => {
const res = await fetch('https://api.example.com/users', {
headers: { Origin: 'https://app.example.com' },
});
expect(res.headers.get('Access-Control-Allow-Origin')).toBe('https://app.example.com');
});
test('CORS blocks evil.com', async () => {
const res = await fetch('https://api.example.com/users', {
headers: { Origin: 'https://evil.com' },
});
expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull();
});Häufige Stolperfallen
Browser zeigt CORS-Fehler — aber der Request ist trotzdem gegangen
Bei Simple Requests sendet der Browser den Request ohne Preflight. Der Server verarbeitet ihn. Erst die Response wird vom Browser blockiert (für JS unleserlich). Bei Side-effect-API (z. B. POST /transfer) ist der Schaden trotzdem passiert. Konsequenz: Server muss eigenständig prüfen (CSRF-Token, SameSite-Cookies).
cors-Package mit origin: true ist Wildcard
Das verbreitete app.use(cors()) ohne Options ist defaultmäßig origin: '*' — Wildcard. Bei API mit Auth-Cookies plus dieser Config ist es harmlos (Browser blockt Credentials bei Wildcard), aber bei API mit Custom-Auth-Header oder JSON-POST kann es überraschend permissiv sein. Explizit Origin-Liste setzen.
Vary: Origin Pflicht bei dynamischem ACAO
Wenn der Server Access-Control-Allow-Origin pro Request unterschiedlich setzt, muss Vary: Origin mit. Sonst kann ein CDN die Response mit Origin-A für einen Request von Origin-B liefern — Cross-Origin-Leak. Cloudflare, Fastly, Varnish — alle berücksichtigen Vary, aber nur wenn gesetzt.
OPTIONS-Endpoints brauchen keine Auth
Klassischer Bug: Auth-Middleware blockt auch OPTIONS-Preflight-Requests. Browser bekommt 401 auf Preflight → kein echter Request. Pattern: OPTIONS vor Auth-Middleware durchschleusen (Express: app.options('*', cors()) ganz oben).
`*` in Allow-Methods und Allow-Headers (CSP-Level-2)
Seit Spec-Update 2020 ist Access-Control-Allow-Methods: * und Access-Control-Allow-Headers: * möglich (CORS 3). Praktisch für lockere APIs, aber explizite Listen sind defensiver — neue Header werden nicht automatisch durchgelassen.
CORS in Local-Dev mit unterschiedlichen Ports
Frontend auf localhost:3000, Backend auf localhost:8080 — Cross-Origin. CORS-Config im Backend muss http://localhost:3000 explizit erlauben. Production-Config mit Domain unterscheidet sich. Pattern: Env-Var CORS_ALLOWED_ORIGINS mit komma-getrennten URLs.
Network-Errors statt CORS-Fehler bei Preflight-Failure
Wenn der Preflight scheitert (Server returns 500 oder timeoutet), bekommt der JavaScript-Client einen generischen Network-Error — nicht „CORS preflight failed". Debug schmerzlich, weil die Ursache schwer zu finden ist. Server-Logs der OPTIONS-Requests sind die erste Anlaufstelle.
Weiterführende Ressourcen
Externe Quellen
- Fetch Standard — CORS Protocol
- MDN — CORS
- PortSwigger — CORS Vulnerabilities
- OWASP — CORS Origin Header Scrutiny
- CORScanner
- Express CORS-Middleware
Verwandte Artikel
- Secure-Headers-Übersicht
- CSP-Deployment
- COOP/COEP/CORP
- CSRF-Grundlagen (Kap 12)
- CSRF in SPAs und APIs (Kap 12)
- Origin und Referer Header (Kap 12)