CORS-Fehlkonfiguration (CWE-942): Wie gespiegelte Origins Daten preisgeben und wie man sie behebt
Eine API, die auf jede Origin mit Access-Control-Allow-Origin gleich dieser Origin und Access-Control-Allow-Credentials: true antwortet, lässt jede Website ihre Antworten mit den Cookies des Besuchers lesen. Wie Suffix-Prüfungen, die null-Origin und gespiegelte Origins die Same-Origin-Policy aushebeln, und welche Allowlist mit exakter Übereinstimmung das behebt, mit einem Beispiel in Express und cors.
Einfache Erklärung (ELI5)
Ein Bankangestellter liest Ihren Kontostand jedem vor, den Sie schicken, solange der Bote einen Brief zeigt, für wen er arbeitet, und die Liste des Angestellten diesen Namen erlaubt. Die Regel eines Angestellten lautet: „Jeden Namen erlauben, der auf das Wort Schmidt endet.“ Ein Fremder schickt einen Boten mit einem Brief von „Goldschmidt“, der Angestellte erlaubt ihn, und Ihr Kontostand wird jemandem vorgelesen, den Sie nie geschickt haben. Das Spiegeln der Origin ist die bequeme Variante, bei der der Angestellte jeden Namen erlaubt. Die Lösung ist eine kurze, exakte Namensliste, Buchstabe für Buchstabe verglichen.
Kernkonzepte & Begriffe
- Same-Origin-Policy
- Browser lassen ein Skript nur Antworten seiner eigenen Origin (Schema, Host und Port) lesen. CORS-Antwortheader sind der Weg, auf dem ein Server diese Regel bewusst für benannte Origins lockert.
- Access-Control-Allow-Origin mit Credentials
- Nennt die Antwort die Origin des Aufrufers in
Access-Control-Allow-Originund sendetAccess-Control-Allow-Credentials: true, kann ein Skript auf dieser Origin Antworten auf Requests lesen, die die Cookies des Nutzers trugen. Browser lehnen*zusammen mit Credentials ab, deshalb spiegeln falsch konfigurierte Server die Origin. - Gespiegelte Origin und Suffix-Prüfung
- Den
Origin-Header in die Antwort zu kopieren vertraut jeder Website. Prüfungen wieendsWith('example.com'), Teilstring-Suchen oder reguläre Ausdrücke ohne Anker und mit unmaskierten Punkten akzeptieren auchhttps://attacker-example.com. - Die null-Origin
- Sandbox-iframes,
data:-URLs und manche Weiterleitungen sendenOrigin: null. Jeder Angreifer kann sie erzeugen,nullzu erlauben heißt also, alle zu erlauben. - Vary: Origin
- Hängt
Access-Control-Allow-Originvom Request ab, muss die AntwortVary: Origintragen, damit gemeinsame Caches die CORS-Header einer Origin nicht an eine andere ausliefern. Das Paketcorsergänzt ihn bei dynamischen Origins.
Schritt-für-Schritt Angriffsablauf
Das Opfer ist bei der Website der API angemeldet
Sein Browser hält ein Sitzungscookie für api.example.com, das bei websiteübergreifenden Requests mitgesendet wird.
Es öffnet eine Seite auf der Domain des Angreifers
Ein Skript ruft fetch('https://api.example.com/api/account', { credentials: 'include' }) auf. Eine Seite auf https://attacker-example.com, die nur die Länge der Antwort in der Konsole ausgibt, zeigt den Fall.
Die API erlaubt die Origin
Die Suffix-Prüfung akzeptiert attacker-example.com, daher trägt die Antwort diese Origin in Access-Control-Allow-Origin zusammen mit Access-Control-Allow-Credentials: true.
Das Skript liest private Daten
Der Browser übergibt das Konto-JSON an das Skript des Angreifers, das es überallhin senden kann. 2016 fand James Kettle dieses Muster bei mehreren Bitcoin-Börsen, wo es API-Schlüssel und Wallet-Daten offenlegte.
Quellcode: Verwundbar vs. Sicher
// server.js: jede Origin auf example.com, dazu null, darf mit Cookies lesen
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';
const app = express();
// true lässt cors die Origin des Aufrufers samt Credentials spiegeln
app.use(cors({
origin: (origin, callback) => callback(null, !origin || origin === 'null' || origin.endsWith('example.com')),
credentials: true,
}));
// private Kontodaten, lesbar für die erlaubten Origins
app.get('/api/account', requireSession, (req, res) => {
res.json(accountFor(req.session.userId));
});
// server.js: zwei exakte Origins, sonst nichts
import express from 'express';
import cors from 'cors';
import { accountFor, requireSession } from './account.js';
const app = express();
// vollständige Origins mit Schema und Host, exakt verglichen
const ALLOWED_ORIGINS = new Set(['https://app.example.com', 'https://admin.example.com']);
// unbekannte oder fehlende Origin bekommt keine CORS-Header; cors ergänzt Vary: Origin
app.use(cors({
origin: (origin, callback) => callback(null, origin !== undefined && ALLOWED_ORIGINS.has(origin)),
credentials: true,
methods: ['GET', 'POST'],
maxAge: 600,
}));
// personenbezogene Daten landen nie in gemeinsamen Caches
app.get('/api/account', requireSession, (req, res) => {
res.set('Cache-Control', 'no-store');
res.json(accountFor(req.session.userId));
});
Checkliste für Engineering & Systemsicherheit
- Eine Allowlist vollständiger Origins (Schema, Host und Port) mit exakter Übereinstimmung führen und per
Set-Abfrage vergleichen;Originnie spiegeln und keine Suffix-, Teilstring- oder ankerlosen Regex-Prüfungen verwenden. - Die
null-Origin nie erlauben und Credentials nie für eine Origin erlauben, die Sie nicht kontrollieren. Vary: Originsenden, wann immerAccess-Control-Allow-Originpro Request gewählt wird, undCache-Control: no-storebei Antworten mit personenbezogenen Daten.- Sitzungscookies auf
SameSite=LaxoderStrictsetzen, damit websiteübergreifende Requests sie nicht mitsenden. - Einen CI-Test ergänzen, der
Origin: https://attacker.exampleundOrigin: nullan authentifizierte Routen sendet und fehlschlägt, wennAccess-Control-Allow-Originzurückkommt.