רק ההערות שלך
חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 18 מתוך 24.
העובדה שהתחברת מוכיחה מי את/ה, אך לא שהפתק שייך לך. GET /notes/:id מוצא פתק לפי המזהה שלו בלבד, ולכן כל מי שמחובר ויש לו מזהה — מקישור משותף או מיומן — יכול לקרוא את הפתק. בדיקת הבעלות נקראת הרשאה.
יש לכלול את הבעלים בשאילתה עצמה, כדי שמסד הנתונים לעולם לא יחזיר מסמך של מישהו אחר. יש להחזיר 404, ולא 403, כשהפתק לא נמצא: 403 היה מגלה למבקר שהפתק קיים.
const order = await orders.findOne({
_id: new ObjectId(req.params.id),
customer: req.user.id,
});
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}כל שאילתה שנוגעת לנתונים של משתמש מסוים צריכה לכלול את המסנן: קריאות, עדכונים ומחיקות.
אתגר
קלהפרק הזה חוזר להתחברות באמצעות הפעלה מפרק 2, והשרת התרחב. כעת יש בו דף בכתובת GET /, נתיבים להערה אחת בכתובות GET /notes/:id ו־DELETE /notes/:id, וחשבון מנהל. יש פרצה בשני הנתיבים של /notes/:id: הם מוצאים הערה לפי המזהה שלה בלבד. כל משתמש מחובר שיודע מזהה יכול לקרוא הערה של מישהו אחר או למחוק אותה. סגור את הפרצה.
- ב־
server.js, בנתיבGET /notes/:id, מצא את ההערה לפי_idשלה ולפיownerשלה יחד. הבעלים חייב להיות המשתמש המחובר,req.user.id. - עשה את אותו הדבר בנתיב
DELETE /notes/:id, כדי שאף אחד לא יוכל למחוק הערה שאינה בבעלותו. - כשאף הערה לא מתאימה, שני הנתיבים כבר משיבים עם קוד מצב 404 ועם
{"error": "Note not found"}. השאר את אותה תגובה גם במקרה של הערה של מישהו אחר. תגובת 403 תודיע למבקר שההערה קיימת.
נסה זאת: לחץ על "שחק".
- שלח
POST /signup, ואזPOST /login, בשניהם עם{"username": "ada", "password": "correct horse"}. - שלח
POST /notesעם{"text": "Safe code 4417"}, והעתק את_idמהתגובה.GET /notes/ואחריו המזהה הזה יחזירו את ההערה. - הירשם והתחבר עם
{"username": "bob", "password": "correct horse"}. שלחGET /notes/ואחריו המזהה של ada: קוד מצב 404 ו־{"error": "Note not found"}. שלחDELETE /notes/ואחריו אותו מזהה: גם כאן תתקבל תגובת 404. - התחבר שוב בתור ada, עם גוף הבקשה שלה מהשלב הראשון. ההערה שלה עדיין שם. מחק אותה כעת באמצעות
DELETE: קוד מצב 204.
נסו בעצמכם
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());
// חשבון המנהל היחיד. הסיסמה שלו מגיעה ממשתני הסביבה.
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, 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();
}
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) });
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) });
if (!deletedCount) {
return res.status(404).json({ error: 'Note not found' });
}
res.status(204).end();
});
// למנהלי מערכת: כמה חשבונות קיימים.
app.get('/admin', requireUser, async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה אימות ואבטחה
תרגלו בעצמכם: Express אונליין