CORS und HTTPOnly-Cookies: Sichere Zugriffstoken
In diesem Beitrag erklären wir, wie Sie CORS (Cross-Origin Resource Sharing) in Verbindung mit HTTPOnly-Cookies verwenden, um Ihre Zugriffstoken optimal zu schützen.
In der heutigen Webentwicklung werden Backend-Server und Frontend-Clients oft auf unterschiedlichen Domains gehostet. Um eine reibungslose Kommunikation zwischen Client und Server zu ermöglichen, ist es unerlässlich, dass der Server CORS aktiviert.
Zudem setzen moderne Architekturen häufig auf zustandslose Authentifizierung, um die Skalierbarkeit zu verbessern. Token werden dabei clientseitig gespeichert, anders als bei serverseitigen Sitzungen. Um die Sicherheit zu erhöhen, sollten diese Token in HTTPOnly-Cookies abgelegt werden.
Warum blockieren Browser Cross-Origin-Anfragen?
Nehmen wir an, Ihre Frontend-Anwendung läuft unter https://app.wdzwdz.com. Ein Skript, das unter dieser Adresse geladen wird, darf standardmäßig nur Ressourcen anfordern, die vom selben Ursprung stammen.
Jeder Versuch, eine Anfrage an eine andere Domain (z.B. https://api.wdzwdz.com), einen anderen Port (z.B. https://app.wdzwdz.com:3000) oder ein anderes Schema (z.B. https://app.wdzwdz.com) zu senden, wird vom Browser blockiert.
Nun stellt sich die Frage, warum solche Anfragen, die vom Browser blockiert werden, problemlos über Tools wie Curl oder Postman funktionieren. Der Grund dafür ist ein Sicherheitsmechanismus, der Nutzer vor CSRF-Angriffen (Cross-Site Request Forgery) schützen soll.
Ein Beispiel: Ein Benutzer ist in seinem PayPal-Konto angemeldet. Wenn nun ein Skript, das auf einer fremden Seite (z.B. https://malicious.com) geladen wurde, ohne CORS-Einschränkungen eine Anfrage an PayPal senden könnte, wäre es für Angreifer ein Leichtes, über eine manipulierte Seite Geld von Benutzerkonten zu transferieren.
Die Angreifer könnten einen bösartigen Link (z.B. https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account) generieren und über eine Kurz-URL tarnen. Wenn ein Benutzer auf diesen Link klickt, wird ein Skript auf der bösartigen Seite ausgeführt und leitet eine Cross-Origin-Anfrage an PayPal, um Geld auf das Konto des Angreifers zu überweisen. Betroffene Benutzer könnten so ihr Geld verlieren, ohne es zu merken.
Aus diesen Gründen blockieren Browser standardmäßig alle Cross-Origin-Anfragen.
Was ist CORS (Cross-Origin Resource Sharing)?
CORS ist ein sicherheitsbasierter Mechanismus, der es dem Server erlaubt, dem Browser mitzuteilen, ob Cross-Origin-Anfragen von vertrauenswürdigen Domains erlaubt sind. Der Server gibt mittels CORS-Headern vor, welche Cross-Origin-Anfragen nicht blockiert werden dürfen.
Wie funktioniert CORS?
Der Server legt in seiner CORS-Konfiguration fest, welche Domains als vertrauenswürdig gelten. Wenn eine Anfrage an den Server gesendet wird, übermittelt die Serverantwort dem Browser, ob die anfragende Domain als vertrauenswürdig eingestuft ist oder nicht.
Es gibt zwei Arten von CORS-Anfragen:
- Einfache Anfrage
- Preflight-Anfrage
Einfache Anfrage:
- Der Browser sendet eine Anfrage an eine Cross-Origin-Domain und fügt einen „Origin“-Header (z.B. https://app.wdzwdz.com) hinzu.
- Der Server sendet eine Antwort zurück, die die zulässigen Methoden und den zulässigen Ursprung angibt.
- Nach dem Erhalt der Antwort überprüft der Browser, ob der gesendete „Origin“-Header und der empfangene „Access-Control-Allow-Origin“-Wert übereinstimmen oder ob ein Platzhalter verwendet wurde. Andernfalls tritt ein CORS-Fehler auf.
Preflight-Anfrage:
- Abhängig von den Parametern der Cross-Origin-Anfrage (wie Methoden wie PUT oder DELETE, benutzerdefinierte Header oder andere Inhaltstypen), sendet der Browser zunächst eine „Preflight“-OPTIONS-Anfrage, um zu überprüfen, ob die eigentliche Anfrage sicher gesendet werden kann.
Nach Erhalt der Antwort (Statuscode 204, was bedeutet, dass kein Inhalt vorhanden ist), prüft der Browser die Access-Control-Parameter. Wenn die Anfrageparameter vom Server erlaubt werden, wird die eigentliche Cross-Origin-Anfrage gesendet und empfangen.
Achtung: Wenn „access-control-allow-origin: *“, ist die Antwort für alle Ursprünge erlaubt, was jedoch nur in Ausnahmefällen und mit Bedacht verwendet werden sollte.
Wie aktiviert man CORS?
Um CORS für eine Domain zu aktivieren, müssen die entsprechenden CORS-Header gesetzt werden. Diese Header legen fest, welche Ursprünge, Methoden, benutzerdefinierten Header und Anmeldeinformationen erlaubt sind.
- Der Browser liest die CORS-Header vom Server und erlaubt tatsächliche Anfragen vom Client nur, nachdem er die Anfrageparameter geprüft hat.
- Access-Control-Allow-Origin: Hier werden spezifische Domains (z.B. https://app.geekflate.com, https://lab.wdzwdz.com) oder ein Platzhalter angegeben.
- Access-Control-Allow-Methods: Definiert die erlaubten HTTP-Methoden (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS).
- Access-Control-Allow-Headers: Hier werden nur bestimmte Header (z.B. Authorization, csrf-token) zugelassen.
- Access-Control-Allow-Credentials: Ein boolescher Wert, der angibt, ob Cross-Origin-Anmeldeinformationen (Cookies, Authorization-Header) erlaubt sind.
Access-Control-Max-Age: Legt fest, wie lange der Browser die Preflight-Antwort zwischenspeichern darf.
Access-Control-Expose-Headers: Gibt Header an, auf die über clientseitige Skripte zugegriffen werden kann.
Im folgenden finden Sie ein Beispiel für eine ExpressJS-Anwendung, die keine CORS-Konfiguration hat:
const express = require('express');
const app = express()
app.get('/users', function (req, res, next) {
res.json({msg: 'user get'})
});
app.post('/users', function (req, res, next) {
res.json({msg: 'user create'})
});
app.put('/users', function (req, res, next) {
res.json({msg: 'User update'})
});
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
CORS in ExpressJS aktivieren
Um CORS in einer ExpressJS-Anwendung zu aktivieren, können Sie das „cors“-Modul nutzen:
npm install cors
Hier ist ein Beispiel, bei dem der Benutzer-API-Endpunkt für POST-, PUT- und GET-Methoden aktiviert ist, aber nicht für die DELETE-Methode:
Um CORS einfach zu aktivieren, verwenden Sie folgendes:
app.use(cors({
origin: '*'
}));
Ein Beispiel mit der „Access-Control-Allow-Origin“-Option:
app.use(cors({
origin: 'https://app.wdzwdz.com'
}));
Um CORS für alle Domains zu aktivieren:
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
]
}));
Um CORS für die Ursprünge https://app.wdzwdz.com und https://lab.wdzwdz.com zu aktivieren, verwenden Sie dies:
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST']
}));
Um CORS nur für bestimmte Methoden zu aktivieren (z.B. GET, POST, PUT):
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token']
}));
Um benutzerdefinierte Header zuzulassen:
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true
}));
Um den Browser anzuweisen, Anmeldeinformationen in der Anfrage zuzulassen:
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600
}));
Um den Browser anzuweisen, die Preflight-Antwort für eine bestimmte Zeit zu speichern:
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600,
exposedHeaders: ['Content-Range', 'X-Content-Range']
}));
Um alle Header, inklusive „Authorization“, anzuzeigen:
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600,
exposedHeaders: ['*', 'Authorization']
}));
Was ist ein HTTP-Cookie?
Ein Cookie ist ein kleines Datenelement, das der Server an den Client-Browser sendet. Bei nachfolgenden Anfragen sendet der Browser die zugehörigen Cookies an dieselbe Domain.
Cookies können verschiedene Attribute haben, die ihr Verhalten definieren:
- Name: Name des Cookies.
- Wert: Daten des Cookies.
- Domain: Cookies werden nur an die definierte Domain gesendet.
- Pfad: Cookies werden nur für den definierten URL-Präfixpfad gesendet.
- Max-Age/Expires: Definiert, wann das Cookie abläuft.
- HTTPOnly (Boolean): Wenn „true“, ist das Cookie nur über den Backend-Server und nicht über clientseitige Skripte zugänglich.
- Secure (Boolean): Wenn „true“, werden Cookies nur über eine SSL/TLS-Verbindung gesendet.
- sameSite(String): Wird verwendet, um das Senden von Cookies bei Cross-Site-Anfragen zu aktivieren/einzuschränken. Akzeptiert die Werte: Strict, Lax, None.
Um den Sicherheitswert von „sameSite“ auf „None“ zu setzen, muss der „Secure“-Wert ebenfalls auf „true“ gesetzt werden.
Warum HTTPOnly-Cookies für Token?
Das Speichern von Zugriffstoken in clientseitigen Speichern (lokaler Speicher, IndexedDB, Cookies ohne „HTTPOnly“-Attribut) ist anfällig für XSS-Angriffe. Wenn Ihre Webseite anfällig für XSS ist, können Angreifer gespeicherte Benutzertoken missbrauchen.
HTTPOnly-Cookies hingegen werden nur vom Server gesetzt und abgerufen und sind nicht auf der Clientseite über JavaScript zugänglich. Dadurch sind sie besser vor XSS-Angriffen geschützt.
- Clientseitige Skripte haben keinen Zugriff auf HTTPOnly-Cookies.
- HTTPOnly-Cookies sind daher sicherer, da sie nur vom Server aus zugänglich sind.
Aktivierung von HTTPOnly-Cookies in einer CORS-fähigen Back-End-Anwendung
- Setzen Sie den „Access-Control-Allow-Credentials“-Header auf „true“.
- „Access-Control-Allow-Origin“ und „Access-Control-Allow-Headers“ sollten keine Platzhalter verwenden.
const express = require('express');
const app = express();
const cors = require('cors');
app.use(cors({
origin: [
'https://app.geekflare.com',
'https://lab.geekflare.com'
],
methods: ['GET', 'PUT', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
credentials: true,
maxAge: 600,
exposedHeaders: ['*', 'Authorization' ]
}));
app.post('/login', function (req, res, next) {
res.cookie('access_token', access_token, {
expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), //second min hour days year
secure: true, // set to true if your using https or samesite is none
httpOnly: true, // backend only
sameSite: 'none' // set to none for cross-request
});
res.json({ msg: 'Login Successfully', access_token });
});
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
});
Das Cookie-Attribut „sameSite“ sollte auf „None“ gesetzt sein.
Um den „sameSite“-Wert auf „None“ zu setzen, muss der „secure“-Wert auf „true“ gesetzt werden. Dies erfordert ein SSL/TLS-Zertifikat.
Hier ist ein Beispielcode, der ein Zugriffstoken in einem HTTPOnly-Cookie setzt:
Die oben gezeigten Schritte können in den meisten Back-End-Sprachen und Webservern implementiert werden.
Cross-Origin-Anfragen mit Anmeldeinformationen (withCredentials)
Um Cross-Origin-Anfragen mit Cookies oder Autorisierungs-Headern zu versenden, müssen Sie die „withCredentials“-Option im Client auf „true“ setzen:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.wdzwdz.com/user', true);
xhr.withCredentials = true;
xhr.send(null);
Hier ein Beispiel für die Verwendung von `fetch()`:
fetch('https://api.wdzwdz.com/user', {
credentials: 'include'
});
Und hier ein Beispiel mit jQuery Ajax:
$.ajax({
url: 'https://api.wdzwdz.com/user',
xhrFields: {
withCredentials: true
}
});
Cookies und Authorization-Header werden standardmäßig für Anfragen innerhalb des gleichen Ursprungs gesendet. Bei Cross-Origin-Anfragen muss „withCredentials“ explizit auf „true“ gesetzt werden.
Beispiel mit Axios:
axios.defaults.withCredentials = true
Zusammenfassung
Dieser Artikel erklärt die Grundlagen von CORS und wie man CORS auf dem Server einrichtet. Zudem haben wir die Bedeutung von HTTPOnly-Cookies und den korrekten Umgang mit Anmeldeinformationen bei Cross-Origin-Anfragen beleuchtet.