Header Keamanan
Termasuk bagian Autentikasi dan Keamanan dalam perjalanan Node.js di Coddy. Pelajaran 20 dari 24.
Browser menebak dan membagikan informasi secara default: browser dapat memperlakukan respons teks sebagai skrip, menjalankan skrip dari mana saja, dan, pada browser lama, mengirimkan alamat halaman lengkapmu ke situs lain. Beberapa header respons dapat menonaktifkan perilaku itu:
X-Content-Type-Options: nosniff: percayaiContent-Type, jangan pernah menebak.Referrer-Policy: no-referrer: jangan beri tahu situs lain halaman mana yang menautkan ke sana.Content-Security-Policy: default-src 'self': muat skrip, style, dan gambar hanya dari situs ini.
Express juga menambahkan X-Powered-By: Express, yang memberi tahu penyerang apa yang perlu dicoba. app.disable('x-powered-by') menghapusnya. Satu middleware mengatur header untuk setiap respons. Middleware ini mengatur dua header lainnya: Strict-Transport-Security memberi tahu browser agar hanya menggunakan HTTPS selama setahun, dan Permissions-Policy menonaktifkan kamera dan mikrofon.
app.use((req, res, next) => {
res.set('Strict-Transport-Security', 'max-age=31536000');
res.set('Permissions-Policy', 'camera=(), microphone=()');
next();
});Tantangan
MudahBrowser membuat tebakan yang bisa dimanfaatkan penyerang. Kirim tiga header bersama setiap respons untuk menonaktifkan tebakan tersebut, dan berhenti memberi tahu semua orang framework apa yang menjalankan servermu.
- Di
server.js, tambahkan middleware sebelum semua route. Middleware ini menetapkan tiga header berikut pada setiap respons, termasuk respons error seperti 401:X-Content-Type-Options: nosniffReferrer-Policy: no-referrerContent-Security-Policy: default-src 'self'. Hanya halaman diGET /yang memerlukan header ini, tetapi tidak masalah jika kamu menetapkannya pada setiap respons.
- Express menambahkan
X-Powered-By: Expresspada setiap respons. Nonaktifkan ini tepat setelah kamu membuat app.
Coba: Tekan Jalankan. Kirim GET /notes tanpa login: statusnya 401. Tab Headers menampilkan X-Content-Type-Options: nosniff dan Referrer-Policy: no-referrer, serta tidak menampilkan X-Powered-By. Kirim POST /signup, lalu POST /login, keduanya dengan {"username": "ada", "password": "correct horse"}. Lalu kirim GET /: kamu mendapatkan halamannya, dengan header yang sama ditambah Content-Security-Policy: default-src 'self'.
Coba sendiri
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());
// Satu-satunya akun admin. Kata sandinya berasal dari environment.
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 });
});
// Mencari user dari cookie sesi, atau mengembalikan 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();
}
// Mengembalikan 403 kecuali user yang masuk memiliki peran ini.
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();
});
// Catatanmu sebagai halaman.
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>`);
});
// Catatan: setiap user hanya membaca dan menulis catatannya sendiri.
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();
});
// Untuk admin: jumlah akun yang ada.
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');
});
Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.
Semua pelajaran di Autentikasi dan Keamanan
1Kata Sandi
Byte dan Teks HeksadesimalMembuat Kunci Kata SandiSalt BaruJangan Simpan Kata SandiMembandingkan Byte KunciMemeriksa Kata SandiAturan Pendaftaran4Pemilik dan Keamanan
Hanya CatatanmuPeran AdminLatihan sendiri: Playground Express