Die Reihenfolge aus dem Requestweg ableiten
Express verarbeitet Middleware in der Reihenfolge ihrer Einbindung. Eine frühere Komponente kann den Request bereits beenden. Verlangt die Anmeldungsmiddleware vor der CORS-Verarbeitung ein Sitzungscookie für OPTIONS, kann der Browser am Preflight scheitern. Die Voranfrage enthält nach dem CORS-Protokoll keine solchen Credentials.
Ein sinnvoller Aufbau für den betreffenden API-Bereich ist daher: CORS-Voranfrage behandeln, dann den eigentlichen Request authentifizieren und autorisieren, Eingaben prüfen und die Fachoperation ausführen. Die CORS-Regel muss zusätzlich auf den vorgesehenen Erfolgs- und Fehlerantworten erhalten bleiben. Allgemeine Schutzgrenzen, etwa Verbindungs- oder Größenbeschränkungen, sind davon nicht pauschal ausgenommen. Siehe Express: Middleware verwenden und cors: Preflight.
Bei app.use('/demo', cors(options)) behandelt die Middleware die passenden Voranfragen des eingebundenen Bereichs. Dafür ist kein zusätzliches pauschales app.options('*', ...) nötig. So hängt das Beispiel auch nicht von unterschiedlichen Wildcard-Pfadsyntaxregeln einzelner Express-Hauptversionen ab.
Zwei lokale Origins, eine synthetische API
Voraussetzungen sind ein separates lokales Arbeitsverzeichnis, Node.js sowie darin verfügbare Express- und cors-Pakete. Erprobt sind Express 5.2.1 und cors 2.8.6 mit Node.js 24.3.0. Der Beispielcode installiert nichts selbst und liest keine Umgebungsgeheimnisse. Speichern Sie den Code als demo.mjs und starten Sie ihn in dieser Umgebung mit node demo.mjs.
Die Ports 43101, 43102 und 43104 müssen frei sein. Alle Listener binden ausschließlich an 127.0.0.1. Der Code setzt keine Sitzung und verarbeitet keine verändernden Aktionen:
import express from 'express';
import cors from 'cors';
const allowedOrigins = new Set(['http://127.0.0.1:43101']);
const api = express();
api.use('/demo', cors({
origin(origin, callback) {
const allowed = typeof origin === 'string' && allowedOrigins.has(origin);
callback(null, allowed ? origin : false);
},
credentials: true,
methods: ['GET'],
allowedHeaders: ['X-Demo'],
exposedHeaders: ['X-Demo-Result'],
maxAge: 5,
}));
api.get('/demo/data', (req, res) => {
res.set('X-Demo-Result', 'synthetic');
res.json({ message: 'Synthetische Antwort' });
});
api.get('/demo/error', (req, res) => {
res.status(503).json({ message: 'Synthetischer Fehler' });
});
const page = `<!doctype html>
<html lang="de">
<meta charset="utf-8">
<title>Lokale CORS-Demo</title>
<h1>Lokale CORS-Demo</h1>
<p>Erlaubter Origin: Port 43101. Gegenprobe: Port 43104.</p>
<button id="data" type="button">Daten lesen</button>
<button id="error" type="button">Fehler lesen</button>
<pre id="output" aria-live="polite"></pre>
<script>
async function load(path) {
const output = document.querySelector('#output');
try {
const response = await fetch('http://127.0.0.1:43102/demo/' + path, {
credentials: 'include',
headers: { 'X-Demo': 'reader' }
});
const data = await response.json();
output.textContent = JSON.stringify({
status: response.status,
result: response.headers.get('X-Demo-Result'),
data
}, null, 2);
} catch (error) {
output.textContent = 'Antwort nicht lesbar: ' + error.message;
}
}
document.querySelector('#data').addEventListener('click', () => load('data'));
document.querySelector('#error').addEventListener('click', () => load('error'));
</script>
</html>`;
const ui = express();
ui.get('/', (req, res) => res.type('html').send(page));
const uiOnly = process.argv.includes('--ui-only');
const servers = [
ui.listen(43101, '127.0.0.1'),
ui.listen(43104, '127.0.0.1'),
];
if (!uiOnly) servers.push(api.listen(43102, '127.0.0.1'));
process.on('SIGINT', () => {
for (const server of servers) server.close();
});
console.log('Demo: http://127.0.0.1:43101 und http://127.0.0.1:43104');
Öffnen Sie die angezeigten Adressen ausdrücklich über 127.0.0.1, nicht abwechselnd über localhost. Das wäre eine andere Origin. Der eigene Requestheader löst einen Preflight aus. credentials: 'include' und credentials: true demonstrieren die zugehörige Freigabebedingung; sie erzeugen keine Anmeldung und die Demo setzt kein Cookie.
Welche Option welche Wirkung hat
Die Originfunktion erlaubt genau den festgelegten lokalen Origin. Für andere Origins und für Requests ohne Origin wird keine entsprechende Freigabe erzeugt. origin: false beendet aber nicht automatisch die Fachanfrage mit einem Zugriffsfehler. Ein normaler GET kann den Handler weiterhin erreichen. Das ist erwartbar: CORS ist keine Autorisierung. Die offizielle cors-Dokumentation weist ausdrücklich auf diese Grenze hin.
methods und allowedHeaders beschreiben die Browserfreigabe des Preflights. Sie implementieren nicht die Methoden- oder Eingabeprüfung der eigentlichen API. exposedHeaders erlaubt dem Browsercode dagegen, den benannten Antwortheader zu lesen. Im Beispiel sind diese beiden Richtungen bewusst verschieden.
maxAge: 5 hält die Demozeit für gespeicherte Voranfragen kurz. Es ist keine allgemeine Produktionsempfehlung. Für eine echte Konfiguration müssen Änderbarkeit, Browsergrenzen und Last des Voranfragewegs zusammen betrachtet werden. Ein erfolgreicher Preflight macht die spätere Antwort zudem nicht automatisch lesbar; diese braucht weiterhin die passenden Header.
Welche Ergebnisse die Demo liefern soll
Die folgenden Ergebnisse wurden am 27. September 2026 auf macOS 26.0.1 in Chromium 147.0.7727.15, Firefox 148.0.2 und Playwright-WebKit 26.4 seriell bestätigt. WebKit ist hier eine Testengine, keine native Safari- oder iPhone-Abnahme. Die Tabelle dient zugleich als kleine Abnahmevorlage für Ihre lokale Wiederholung:
Bei schmaler Ansicht können Sie die Tabelle seitlich scrollen.
| Eigener Aufruf | Erwartung |
|---|---|
| Port 43101, „Daten lesen“ | Preflight freigegeben, anschließend JSON mit Status 200 und lesbarem X-Demo-Result |
| Port 43101, „Fehler lesen“ | Antwort mit Status 503 und vorgesehenem JSON-Fehler lesbar |
| Port 43104, derselbe Browseraufruf | Preflight nicht freigegeben, deshalb kein nachfolgender GET und keine lesbare Antwort |
| GET ohne Origin aus einem lokalen HTTP-Werkzeug | Synthetische Daten sind abrufbar; zeigt gerade die fehlende Authentifizierung der Demo |
Prüfen Sie im Browser Voranfrage und Folgeanfrage getrennt. Dokumentieren Sie außerdem die tatsächlich ankommenden Header. Wenn die Demo hinter einem eigenen nginx-Proxy läuft, darf dieser keine widersprüchliche zweite Originfreigabe hinzufügen. Für diesen Fall hilft CORS in nginx.
Die zusätzliche Gegenprobe ohne X-Demo zeigt die wichtige Grenze: Ein einfacher GET aus Port 43104 erreichte den Handler, doch JavaScript durfte seine Antwort nicht lesen. Mit einem nicht freigegebenen Requestheader (X-Other) scheiterte dagegen auch die erlaubte Origin am Preflight. credentials: 'include' funktionierte mit der konkreten Origin und Access-Control-Allow-Credentials: true; weder Anmeldung noch echte Cookies wurden erzeugt oder geprüft.
Für die lokale nginx-Demo kann dasselbe Programm nur die Browseroberflächen bereitstellen: Beenden Sie zuerst einen laufenden vollständigen Demoprozess mit Ctrl+C und starten Sie dann node demo.mjs --ui-only. Dieser Modus öffnet ausschließlich die Ports 43101 und 43104; die API auf 43102 kommt dann von Ihrer separat gestarteten nginx-Demo. In der nginx-Fassung ist kein X-Demo-Result vorgesehen, deshalb darf das angezeigte Feld result dort null sein.
Beenden Sie die Demo im gestarteten Terminal mit Ctrl+C. Die zu diesem Prozess gehörenden Listener werden geschlossen. Die lokale HTTP-Demo ist keine veröffentlichbare Anwendung und kein getesteter Cookie- oder CSRF-Schutz.
Die Regel in eine echte API übernehmen
Ersetzen Sie Demo-Origin, Routen und Header durch den tatsächlichen Vertrag. Benötigt der Client einen Bearer-Header, erlauben Sie Authorization ausdrücklich. Nutzen Sie credentials: true nur für das vorgesehene Credentials-Modell und konkrete vertrauenswürdige Origins. Ein öffentliches Datenangebot kann dagegen eine andere, bewusst offene CORS-Regel haben.
Fügen Sie die bestehende Authentifizierung, Objektberechtigung und Eingabeprüfung auf dem eigentlichen Requestweg hinzu. Bei cookiebasierten Änderungen bleibt CSRF-Schutz nötig. Werden Regeln aus einer Datenbank geladen, definieren Sie ein klares Fehlerverhalten. Ein Ladefehler darf nicht als Freigabe aller Origins behandelt werden.
Prüfen Sie während der Einführung die erlaubten Clients sowie Ausnahmen und Fehlerfälle. Sichern Sie den letzten sicheren Konfigurationsstand als Rückweg. Erweitern Sie bei einem Fehler nicht ungeprüft die Liste oder setzen origin: true zusammen mit Credentials. Die CORS-Fehlerdiagnose hilft, die betroffene Schicht zu finden.
Der API Security Check beobachtet öffentliche Headerantworten. Er prüft weder die Reihenfolge Ihrer Express-Middleware noch angemeldete Browser- oder CSRF-Abläufe. Quellenstand: 27. September 2026.
Quellen und Stand
Quellenstand: 27. September 2026. Die Links stehen zusätzlich bei den jeweils begründeten Aussagen.