אבטחה: 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);

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

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה עושה helmet()?

    1. חוסם כל בקשה
    2. בודק קלט
    3. מוסיף Headers של אבטחה נגד התקפות נפוצות
    4. מצפין את השרת
    הצגת התשובה

    תשובה ג. שורה אחת, כתריסר הגנות.

  2. ממה מגן Rate Limiting?

    1. מ-XSS
    2. מ-SQL Injection
    3. מבאגים
    4. מניחוש סיסמאות בכמויות ומהעמסת השרת
    הצגת התשובה

    תשובה ד. הגבלה של מספר בקשות לכל IP בחלון זמן.

  3. האם CORS מונע ממישהו לפנות לשרת עם curl?

    1. לא: CORS הוא הגנה של הדפדפן בלבד
    2. רק עם HTTPS
    3. כן
    4. רק ב-POST
    הצגת התשובה

    תשובה א. לכן אימות והרשאות תמיד בשרת, לא רק CORS.