Sicherheits-Header
Teil des Abschnitts Authentifizierung und Sicherheit der Node.js-Journey von Coddy. Lektion 20 von 24.
Browser raten und teilen standardmäßig: Sie können eine Textantwort als Skript behandeln, Skripte von überall ausführen und in älteren Browsern anderen Websites die vollständige Adresse deiner Seite senden. Einige Antwort-Header schalten das ab:
X-Content-Type-Options: nosniff: Vertraue aufContent-Type, rate niemals.Referrer-Policy: no-referrer: Teile anderen Websites nicht mit, von welcher Seite aus auf sie verlinkt wurde.Content-Security-Policy: default-src 'self': Lade Skripte, Styles und Bilder nur von dieser Website.
Express fügt außerdem X-Powered-By: Express hinzu, wodurch Angreifer erfahren, was sie ausprobieren können. app.disable('x-powered-by') entfernt diesen Header. Eine Middleware setzt Header für jede Antwort. Diese hier setzt zwei weitere Header: Strict-Transport-Security weist den Browser an, ein Jahr lang nur HTTPS zu verwenden, und Permissions-Policy schaltet Kamera und Mikrofon aus.
app.use((req, res, next) => {
res.set('Strict-Transport-Security', 'max-age=31536000');
res.set('Permissions-Policy', 'camera=(), microphone=()');
next();
});Aufgabe
EinfachBrowser stellen Vermutungen an, die Angreifer ausnutzen können. Sende mit jeder Antwort drei Header, um diese Vermutungen zu unterbinden, und verrate nicht mehr allen, welches Framework auf deinem Server läuft.
- Füge in
server.jsvor allen Routen eine Middleware hinzu. Sie setzt diese drei Header bei jeder Antwort, auch bei Fehlerantworten wie einem 401:X-Content-Type-Options: nosniffReferrer-Policy: no-referrerContent-Security-Policy: default-src 'self'. Nur die Seite unterGET /benötigt diesen Header, aber ihn bei jeder Antwort zu setzen, ist in Ordnung.
- Express fügt jeder Antwort
X-Powered-By: Expresshinzu. Schalte das direkt nach dem Erstellen der App aus.
Probiere es aus: Klicke auf „Run“. Sende GET /notes, ohne dich anzumelden: Status 401. Im Tab „Headers“ werden X-Content-Type-Options: nosniff und Referrer-Policy: no-referrer angezeigt, aber kein X-Powered-By. Sende POST /signup und dann POST /login, bei beiden mit {"username": "ada", "password": "correct horse"}. Sende dann GET /: Du erhältst die Seite mit denselben Headern und zusätzlich Content-Security-Policy: default-src 'self'.
Probier es selbst
import express from 'express';
import { ObjectId } from 'mongodb';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const app = express();
app.use(express.json());
// Das einzige Administratorkonto. Sein Passwort stammt aus der Umgebung.
await users.insertOne({ username: 'admin', role: 'admin', passwordHash: await hashPassword(process.env.ADMIN_PASSWORD) });
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, role: 'member', passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
const sid = randomBytes(24).toString('base64url');
await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
res.cookie('sid', sid, {
httpOnly: true,
sameSite: 'lax',
secure: true,
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ username: user.username });
});
// Findet den Benutzer anhand des Session-Cookies oder antwortet mit 401.
async function requireUser(req, res, next) {
const { sid } = parseCookies(req.get('cookie'));
const session = sid && (await sessions.findOne({ _id: sid }));
const user = session && (await users.findOne({ _id: session.userId }));
if (!user) {
return res.status(401).json({ error: 'Log in first' });
}
req.user = { id: user._id.toHexString(), username: user.username, role: user.role };
next();
}
// Antwortet mit 403, wenn der angemeldete Benutzer nicht diese Rolle hat.
function requireRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Not allowed' });
}
next();
};
}
app.get('/me', requireUser, (req, res) => {
res.json({ username: req.user.username });
});
app.post('/logout', async (req, res) => {
const { sid } = parseCookies(req.get('cookie'));
if (sid) {
await sessions.deleteOne({ _id: sid });
}
res.clearCookie('sid');
res.status(204).end();
});
// Deine Notizen als Webseite.
app.get('/', requireUser, async (req, res) => {
const list = await notes.find({ owner: req.user.id }).toArray();
const items = list.map((note) => `<li>${note.text}</li>`).join('');
res.send(`<!doctype html><title>Vault</title><h1>${req.user.username}'s notes</h1><ul>${items}</ul>`);
});
// Notizen: Jeder Benutzer liest und schreibt nur seine eigenen.
app.get('/notes', requireUser, async (req, res) => {
res.json(await notes.find({ owner: req.user.id }).toArray());
});
app.post('/notes', requireUser, async (req, res) => {
const note = { owner: req.user.id, text: String(req.body.text ?? '') };
await notes.insertOne(note);
res.status(201).json(note);
});
app.get('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const note = await notes.findOne({ _id: new ObjectId(req.params.id), owner: req.user.id });
if (!note) {
return res.status(404).json({ error: 'Note not found' });
}
res.json(note);
});
app.delete('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const { deletedCount } = await notes.deleteOne({ _id: new ObjectId(req.params.id), owner: req.user.id });
if (!deletedCount) {
return res.status(404).json({ error: 'Note not found' });
}
res.status(204).end();
});
// Für Administratoren: wie viele Konten es gibt.
app.get('/admin', requireUser, requireRole('admin'), async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Authentifizierung und Sicherheit
1Passwörter
Bytes und Hex-TextEinen Passwortschlüssel ableitenEin neues SaltPasswörter niemals speichernSchlüssel-Bytes vergleichenEin Passwort überprüfenRegeln für die Registrierung4Eigentümer und Sicherheit
Nur deine NotizenAdmin-RollenÜbe selbstständig: Express-Playground