אבטחה: helmet, CORS, הגבלת קצב ואימות קלט
שרת שפתוח לאינטרנט מקבל בקשות גם ממי שמנסה לפרוץ אותו. החדשות הטובות: רוב ההגנות הבסיסיות הן Middleware מוכן ושורה אחת של קוד. השיעור עובר על ארבע שכבות שכל API צריך.
npm install helmet cors express-rate-limit zod
1. helmet: Headers של אבטחה
import helmet from 'helmet';
app.use(helmet());
שורה אחת מוסיפה כתריסר Headers שמגנים מפני התקפות נפוצות: מונעת מאתרים אחרים להטמיע את האתר שלכם ב-iframe (clickjacking), מגבילה מאיפה מותר לטעון סקריפטים (Content-Security-Policy), ומסתירה שהשרת רץ על Express.
2. CORS: מי רשאי לפנות ל-API
דפדפן חוסם בקשה מ-https://my-app.com ל-https://api.my-app.com, אלא אם השרת מתיר זאת במפורש. זה CORS. מתירים רק את מה שצריך:
import cors from 'cors';
app.use(cors({
origin: ['https://my-app.com', 'http://localhost:5173'], // האתר בפרודקשן + Vite בפיתוח
credentials: true, // אם שולחים cookies
}));
אל תכתבו cors() בלי הגדרות ב-API עם משתמשים: זה מתיר לכל אתר בעולם לפנות אליו. חשוב גם לזכור ש-CORS מגן על משתמשים בדפדפן בלבד. הוא לא מונע מאף אחד לפנות לשרת עם curl או סקריפט.
3. הגבלת קצב (Rate Limiting)
בלי הגבלה, תוקף יכול לנסות אלפי סיסמאות בדקה, או להפיל את השרת בעומס. מגבילים כמה בקשות מותר מכל כתובת IP:
import { rateLimit } from 'express-rate-limit';
// הגבלה כללית לכל ה-API
app.use('/api', rateLimit({
windowMs: 15 * 60 * 1000, // חלון של 15 דקות
limit: 300, // עד 300 בקשות לכל IP בחלון
standardHeaders: 'draft-8',
legacyHeaders: false,
}));
// הגבלה קשוחה בהרבה על התחברות
app.use('/api/auth/login', rateLimit({
windowMs: 15 * 60 * 1000,
limit: 10,
message: { error: 'יותר מדי ניסיונות התחברות. נסו שוב בעוד רבע שעה.' },
}));
כשהשרת יושב מאחורי שירות אחסון או Proxy, צריך גם app.set('trust proxy', 1), אחרת כל הבקשות ייראו כאילו הגיעו מאותה כתובת. על זה בשיעור הבא.
4. אימות קלט עם Zod
הכלל: כל מה שמגיע מהלקוח חשוד, עד שבדקתם אותו. במקום עשרות if, מגדירים סכמה פעם אחת:
import { z } from 'zod';
const createTodoSchema = z.object({
title: z.string().trim().min(1, 'חובה לכתוב כותרת').max(200),
dueDate: z.coerce.date().optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
});
// Middleware שמאמת את req.body לפי סכמה
function validateBody(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'קלט לא תקין',
details: result.error.issues.map((issue) => ({
field: issue.path.join('.'),
message: issue.message,
})),
});
}
req.body = result.data; // נתונים נקיים: חתוכים, מומרים, עם ברירות מחדל
next();
};
}
todosRouter.post('/', validateBody(createTodoSchema), async (req, res) => {
const todo = await service.createTodo(req.body.title);
res.status(201).json(todo);
});
בונוס: Zod עובד גם בצד הלקוח, וגם מייצר טיפוסי TypeScript מהסכמה. אותה סכמה יכולה לאמת את הטופס ב-React ואת הבקשה בשרת.
עוד כמה כללים
- SQL Injection - תמיד שאילתות עם
?(ראו חיבור למסד נתונים). - XSS - בתבניות,
<%=ולא<%-. ב-React זה מובנה, כל עוד לא משתמשים ב-dangerouslySetInnerHTML. - גודל בקשה -
express.json({ limit: '100kb' })מונע שליחת גוף ענק שמעמיס על השרת. - סודות - מפתחות וסיסמאות רק ב-
.env, ו-.envב-.gitignore. - תלויות -
npm auditמציג חבילות עם פרצות ידועות. הריצו אותו מדי פעם, ושדרגו. - שגיאות - בפרודקשן לא מחזירים ללקוח stack trace או הודעות פנימיות (ראו טיפול בשגיאות).
הסדר הנכון ב-app.js
app.set('trust proxy', 1); // אם יש Proxy לפני השרת
app.use(helmet());
app.use(cors({ origin: allowedOrigins, credentials: true }));
app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, limit: 300 }));
app.use(express.json({ limit: '100kb' }));
app.use('/api/auth', authRouter);
app.use('/api/todos', requireAuth, todosRouter);
app.use(notFoundHandler);
app.use(errorHandler);
ההגנות באות לפני הנתיבים, כדי שבקשה חשודה תיעצר לפני שהיא מגיעה לקוד שלכם.
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה עושה
helmet()?הצגת התשובה
תשובה ג. שורה אחת, כתריסר הגנות.
-
ממה מגן Rate Limiting?
הצגת התשובה
תשובה ד. הגבלה של מספר בקשות לכל IP בחלון זמן.
-
האם CORS מונע ממישהו לפנות לשרת עם curl?
הצגת התשובה
תשובה א. לכן אימות והרשאות תמיד בשרת, לא רק CORS.