Menu
CoddyTech

Güvenlik Başlıkları

Coddy'nin Node.js Journey'sinin Kimlik Doğrulama ve Güvenlik bölümünün bir parçası. Ders 20 / 24.

Tarayıcılar varsayılan olarak tahminde bulunur ve bilgi paylaşır: metin yanıtını betik olarak değerlendirebilir, her yerden gelen betikleri çalıştırabilir ve eski tarayıcılarda tam sayfa adresinizi başka sitelere gönderebilirler. Birkaç yanıt başlığı bu davranışları kapatır:

  • X-Content-Type-Options: nosniff: Content-Type değerine güvenin, asla tahminde bulunmayın.
  • Referrer-Policy: no-referrer: diğer sitelere, onları hangi sayfanın bağlantı verdiğini bildirmeyin.
  • Content-Security-Policy: default-src 'self': betikleri, stilleri ve görselleri yalnızca bu siteden yükleyin.

Express ayrıca saldırganlara neyi deneyeceklerini gösteren X-Powered-By: Express başlığını da ekler. app.disable('x-powered-by') bu başlığı kaldırır. Bir ara yazılım, her yanıt için başlıkları ayarlar. Bu ara yazılım iki başlık daha ayarlar: Strict-Transport-Security tarayıcıya bir yıl boyunca yalnızca HTTPS kullanmasını söyler ve Permissions-Policy kamera ile mikrofonu kapatır.

app.use((req, res, next) => {
    res.set('Strict-Transport-Security', 'max-age=31536000');
    res.set('Permissions-Policy', 'camera=(), microphone=()');
    next();
});
challenge icon

Görev

Kolay

Tarayıcılar, saldırganların kullanabileceği tahminlerde bulunur. Bu tahminleri devre dışı bırakmak için her yanıtla birlikte üç başlık gönderin ve sunucunuzda hangi çerçevenin çalıştığını herkese bildirmeyi bırakın.

  1. server.js dosyasında, tüm rotalardan önce bir ara katman yazılımı ekleyin. Bu yazılım, 401 gibi hata yanıtları da dahil olmak üzere her yanıta şu üç başlığı ekler:
    • X-Content-Type-Options: nosniff
    • Referrer-Policy: no-referrer
    • Content-Security-Policy: default-src 'self'. Bu başlık yalnızca GET / sayfası için gereklidir, ancak her yanıta eklenmesinde sakınca yoktur.
  2. Express, her yanıta X-Powered-By: Express ekler. Uygulamayı oluşturduktan hemen sonra bunu devre dışı bırakın.

Deneyin: “Çalıştır”a basın. Oturum açmadan GET /notes isteği gönderin: durum kodu 401. Headers sekmesinde X-Content-Type-Options: nosniff ve Referrer-Policy: no-referrer başlıkları görünür; X-Powered-By görünmez. {"username": "ada", "password": "correct horse"} ile POST /signup, ardından POST /login isteği gönderin. Sonra GET / isteği gönderin: sayfa, aynı başlıklarla birlikte Content-Security-Policy: default-src 'self' başlığıyla gelir.

Kendin dene

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());

// Tek admin hesabı. Parolası ortam değişkeninden alınır.
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 });
});

// Oturum çerezinin ait olduğu kullanıcıyı bulur; bulamazsa 401 yanıtı verir.
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();
}

// Oturum açmış kullanıcı bu role sahip değilse 403 yanıtı verir.
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();
});

// Notlarınız bir sayfa olarak.
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>`);
});

// Notlar: Her kullanıcı yalnızca kendi notlarını okur ve yazar.
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();
});

// Yöneticiler için: Kaç hesap olduğu.
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');
});
quiz iconKendini test et

Bu ders kısa bir quiz içerir. Soruları yanıtlamak ve ilerlemeni kaydetmek için derse başla.

Kimlik Doğrulama ve Güvenlik bölümündeki tüm dersler

Kendi başına pratik yap: Express playground