Перейти к содержимому

16. CORS и Security headers

Иллюстрация к уроку

Безопасность API — это не опция, это обязательный минимум. CORS, заголовки, защита от атак.

CORS нужен когда фронт и бек на разных доменах:

Frontend: https://myapp.com → запрос на →
Backend: https://api.myapp.com ← нужен CORS ←
Окно терминала
npm install cors
const cors = require('cors');
// Разрешить всем (только для разработки!)
app.use(cors());
// Настроенный CORS
app.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' })
);
// Динамический CORS
const 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 автоматически устанавливает HTTP заголовки безопасности:

Окно терминала
npm install helmet
const 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,
}));
Окно терминала
npm install express-rate-limit
const 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 HTML
const 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 Pollution
const hpp = require('hpp');
app.use(hpp());
// 4. Ограничение размера тела запроса
app.use(express.json({ limit: '10kb' })); // 10 КБ для JSON
app.use(express.urlencoded({ limit: '10kb', extended: true }));
// Принудительный HTTPS
app.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 HSTS
app.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 headers
app.use(helmet());
// 2. ✅ CORS
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true,
}));
// 3. ✅ Rate limiting
app.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 injection
app.use(mongoSanitize());
// 6. ✅ Защита от parameter pollution
app.use(hpp());
// 7. ✅ Сжатие ответов
app.use(compression());
// 8. ✅ Скрываем информацию о сервере
app.disable('x-powered-by'); // убираем X-Powered-By: Express
module.exports = app;
  1. Настрой CORS с белым списком доменов из переменной окружения
  2. Подключи helmet с базовыми настройками
  3. Создай строгий rate limiter для /auth/login (5 попыток за 15 мин)
  4. Добавь принудительный HTTPS редирект в production
  5. Составь полный чеклист безопасности для своего API и реализуй его