Menu
CoddyTech

הגבלת קצב ניסיונות ההתחברות

חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 16 מתוך 17.

סקריפט יכול לנסות אלפי סיסמאות בדקה עבור חשבון אחד. ספרו את ניסיונות ההתחברות הכושלים לכל שם משתמש, ואחרי חמישה ניסיונות בדקה סרבו לבדוק ניסיונות נוספים עד שתסתיים הדקה.

הסטטוס 429 Too Many Requests מסביר מדוע, והכותרת Retry-After מציינת כמה שניות צריך להמתין. ספרו רק ניסיונות כושלים: אדם שמקליד את הסיסמה הנכונה לעולם לא אמור לנעול את עצמו מחוץ לחשבון. Map בזיכרון מספיק לשרת אחד; הוא מתרוקן כשהשרת מופעל מחדש.

const lastSent = new Map();

app.post('/reset', (req, res) => {
    const email = String(req.body.email ?? '').toLowerCase();
    const wait = (lastSent.get(email) ?? 0) + 60_000 - Date.now();
    if (wait > 0) {
        res.set('Retry-After', String(Math.ceil(wait / 1000)));
        return res.status(429).json({ error: 'One reset email a minute' });
    }
    lastSent.set(email, Date.now());
    // ... שולחים את הודעת האימייל
});
challenge icon

אתגר

קל
  • אחרי 5 ניסיונות התחברות כושלים עם שם משתמש אחד בתוך דקה, POST /login מחזיר 429 { error } עבור אותו שם, גם אם הסיסמה נכונה.
  • בתגובת 429 מופיע Retry-After: מספר השניות שנותרו באותה דקה.
  • התחברויות מוצלחות לעולם אינן נספרות.

הירשם, שלח שש סיסמאות שגויות ואז את הסיסמה הנכונה.

נסו בעצמכם

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.disable('x-powered-by');
app.use(express.json());

// כותרות שמורות לדפדפן לנהוג בזהירות עם כל תגובה.
app.use((req, res, next) => {
    res.set({
        'X-Content-Type-Options': 'nosniff',
        'Referrer-Policy': 'no-referrer',
        'Content-Security-Policy': "default-src 'self'",
    });
    next();
});

// חשבון מנהל המערכת היחיד. הסיסמה שלו מגיעה מהסביבה.
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 });
});

// מאתר את המשתמש המשויך לעוגיית ההפעלה, או מחזיר 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();
}

// מחזיר 403 אלא אם למשתמש המחובר יש את התפקיד הזה.
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();
});

// ההערות שלך כדף.
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>`);
});

// הערות: כל משתמש קורא וכותב רק את ההערות שלו.
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();
});

// למנהלי מערכת: כמה חשבונות קיימים.
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');
});

כל השיעורים ביחידה אימות ואבטחה