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)