16. CORS и Security headers

Безопасность API — это не опция, это обязательный минимум. CORS, заголовки, защита от атак.
CORS (Cross-Origin Resource Sharing)
Заголовок раздела «CORS (Cross-Origin Resource Sharing)»CORS нужен когда фронт и бек на разных доменах:
Frontend: https://myapp.com → запрос на →Backend: https://api.myapp.com ← нужен CORS ←npm install corsconst cors = require('cors');
// Разрешить всем (только для разработки!)app.use(cors());
// Настроенный CORSapp.use(cors({ // Разрешённые источники origin: [ 'https://myapp.com', 'https://admin.myapp.com', 'http://localhost:3000', 'http://localhost:5173', ],
// Разрешённые методы methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
// Разрешённые заголовки allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
// Разрешаем куки и авторизацию credentials: true,
// Кэш preflight запроса (секунды) maxAge: 86400,}));
// CORS для конкретного роутаapp.get('/public-data', cors({ origin: '*' }), // разрешаем всем (req, res) => res.json({ data: 'public' }));
// Динамический CORSconst allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
app.use(cors({ origin: (origin, callback) => { // Разрешаем запросы без origin (curl, Postman) if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error(`Origin ${origin} не разрешён`)); } }, credentials: true,}));Helmet — Security Headers
Заголовок раздела «Helmet — Security Headers»Helmet автоматически устанавливает HTTP заголовки безопасности:
npm install helmetconst helmet = require('helmet');
// Базовая защита (рекомендуется)app.use(helmet());
// Что добавляет helmet:// Content-Security-Policy — защита от XSS// X-Content-Type-Options: nosniff — защита от MIME sniffing// X-Frame-Options: SAMEORIGIN — защита от clickjacking// X-XSS-Protection: 0 — отключает старый XSS фильтр// Strict-Transport-Security — принудительный HTTPS// Referrer-Policy — контроль Referer заголовка// Permissions-Policy — ограничение API браузера
// Тонкая настройкаapp.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", "cdn.example.com"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], }, }, crossOriginEmbedderPolicy: false, // если нужен iframe}));
// Для API (без HTML) CSP не нужнаapp.use(helmet({ contentSecurityPolicy: false,}));Rate Limiting — защита от DDoS
Заголовок раздела «Rate Limiting — защита от DDoS»npm install express-rate-limitconst rateLimit = require('express-rate-limit');
// Глобальный лимитconst globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 минут max: 100, // 100 запросов standardHeaders: true, // Rate-Limit-* заголовки legacyHeaders: false, message: { error: 'Слишком много запросов. Попробуй через 15 минут.', retryAfter: 15 * 60, },});
// Строгий лимит для авторизацииconst authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // только 5 попыток! skipSuccessfulRequests: true, // не считаем успешные входы message: { error: 'Слишком много попыток входа. Подождите 15 минут.' },});
// Лимит на загрузку файловconst uploadLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 час max: 50, message: { error: 'Лимит загрузок на сегодня исчерпан' },});
app.use(globalLimiter);app.use('/api/auth/login', authLimiter);app.use('/api/auth/register', authLimiter);app.use('/api/upload', uploadLimiter);Защита от распространённых атак
Заголовок раздела «Защита от распространённых атак»// 1. NoSQL Injection — для MongoDB// Используй mongoose (автоматически) или sanitize вручнуюconst mongoSanitize = require('express-mongo-sanitize');app.use(mongoSanitize()); // удаляет $ и . из req.body
// 2. XSS — sanitize HTMLconst xss = require('xss');
function sanitizeInput(data) { if (typeof data === 'string') return xss(data); if (typeof data === 'object' && data !== null) { return Object.fromEntries( Object.entries(data).map(([k, v]) => [k, sanitizeInput(v)]) ); } return data;}
app.use((req, res, next) => { if (req.body) req.body = sanitizeInput(req.body); next();});
// 3. Parameter Pollutionconst hpp = require('hpp');app.use(hpp());
// 4. Ограничение размера тела запросаapp.use(express.json({ limit: '10kb' })); // 10 КБ для JSONapp.use(express.urlencoded({ limit: '10kb', extended: true }));HTTPS в production
Заголовок раздела «HTTPS в production»// Принудительный HTTPSapp.use((req, res, next) => { if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') { return res.redirect(301, `https://${req.hostname}${req.url}`); } next();});
// Или через helmet HSTSapp.use(helmet({ hsts: { maxAge: 31536000, // 1 год includeSubDomains: true, preload: true, },}));Чеклист безопасности
Заголовок раздела «Чеклист безопасности»// app.js — финальная конфигурацияconst express = require('express');const cors = require('cors');const helmet = require('helmet');const rateLimit = require('express-rate-limit');const mongoSanitize = require('express-mongo-sanitize');const hpp = require('hpp');const compression = require('compression');
const app = express();
// 1. ✅ Security headersapp.use(helmet());
// 2. ✅ CORSapp.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(','), credentials: true,}));
// 3. ✅ Rate limitingapp.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 200 }));
// 4. ✅ Парсинг с лимитомapp.use(express.json({ limit: '10kb' }));app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// 5. ✅ Sanitize от NoSQL injectionapp.use(mongoSanitize());
// 6. ✅ Защита от parameter pollutionapp.use(hpp());
// 7. ✅ Сжатие ответовapp.use(compression());
// 8. ✅ Скрываем информацию о сервереapp.disable('x-powered-by'); // убираем X-Powered-By: Express
module.exports = app;Практика
Заголовок раздела «Практика»- Настрой CORS с белым списком доменов из переменной окружения
- Подключи helmet с базовыми настройками
- Создай строгий rate limiter для
/auth/login(5 попыток за 15 мин) - Добавь принудительный HTTPS редирект в production
- Составь полный чеклист безопасности для своего API и реализуй его