Header di sicurezza
Fa parte della sezione Autenticazione e sicurezza del percorso Node.js di Coddy. Lezione 20 di 24.
I browser indovinano e condividono per impostazione predefinita: possono trattare una risposta di testo come uno script, eseguire script da qualsiasi posizione e, nei browser meno recenti, inviare l’indirizzo completo della tua pagina ad altri siti. Alcune intestazioni di risposta disattivano questi comportamenti:
X-Content-Type-Options: nosniff: fidati diContent-Type, non indovinare mai.Referrer-Policy: no-referrer: non comunicare agli altri siti quale pagina li ha collegati.Content-Security-Policy: default-src 'self': carica script, fogli di stile e immagini solo da questo sito.
Express aggiunge anche X-Powered-By: Express, che indica agli aggressori cosa provare. app.disable('x-powered-by') la rimuove. Un middleware imposta le intestazioni per ogni risposta. Questo imposta altre due intestazioni: Strict-Transport-Security indica al browser di usare solo HTTPS per un anno, mentre Permissions-Policy disattiva la fotocamera e il microfono.
app.use((req, res, next) => {
res.set('Strict-Transport-Security', 'max-age=31536000');
res.set('Permissions-Policy', 'camera=(), microphone=()');
next();
});Sfida
FacileI browser fanno supposizioni che gli aggressori possono sfruttare. Invia tre intestazioni con ogni risposta per disattivare queste supposizioni e smetti di dire a tutti quale framework esegue il tuo server.
- In
server.js, aggiungi un middleware prima di tutte le route. Imposta queste tre intestazioni su ogni risposta, comprese le risposte di errore come 401:X-Content-Type-Options: nosniffReferrer-Policy: no-referrerContent-Security-Policy: default-src 'self'. Solo la pagina suGET /ne ha bisogno, ma va bene impostarla su ogni risposta.
- Express aggiunge
X-Powered-By: Expressa ogni risposta. Disattivalo subito dopo aver creato l'app.
Prova: premi «Gioca». Invia GET /notes senza aver effettuato l'accesso: stato 401. La scheda Headers mostra X-Content-Type-Options: nosniff e Referrer-Policy: no-referrer, e non mostra X-Powered-By. Invia POST /signup, poi POST /login, entrambi con {"username": "ada", "password": "correct horse"}. Poi invia GET /: ricevi la pagina, con le stesse intestazioni più Content-Security-Policy: default-src 'self'.
Provalo tu
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());
// L'unico account admin. La sua password proviene dall'ambiente.
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 });
});
// Trova l'utente associato al cookie di sessione, altrimenti risponde con 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();
}
// Risponde con 403 se l'utente che ha effettuato l'accesso non ha questo ruolo.
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();
});
// Le tue note visualizzate in una pagina.
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>`);
});
// Note: ogni utente legge e scrive solo le proprie.
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();
});
// Per gli amministratori: quanti account esistono.
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');
});
Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.
Tutte le lezioni di Autenticazione e sicurezza
1Password
Byte e testo esadecimaleDerivare una chiave dalla passwordUn nuovo saltNon memorizzare mai le passwordConfrontare i byte della chiaveVerificare una passwordRegole di registrazione4Proprietari e sicurezza
Solo le tue noteRuoli di amministratoreEsercitati da solo: Playground Express