15. Best Practices: чеклист
Чеклист: Пароли и хранение
Заголовок раздела «Чеклист: Пароли и хранение»☐ Пароли хэшируются bcrypt (rounds ≥ 12) или Argon2id☐ Пароли НИКОГДА не логируются☐ Пароли НИКОГДА не хранятся в открытом виде☐ Минимальные требования к паролю: 8+ символов☐ Проверка пароля на утечку (HaveIBeenPwned API)☐ Ограничение попыток входа (rate limiting)☐ Временная блокировка после N неудачных попыток☐ API ключи и секреты в .env, НЕ в кодеHaveIBeenPwned API интеграция
Заголовок раздела «HaveIBeenPwned API интеграция»import crypto from 'crypto';
async function isPasswordPwned(password) { // Хэшируем пароль и отправляем только первые 5 символов SHA1 const sha1 = crypto.createHash('sha1').update(password).digest('hex').toUpperCase(); const prefix = sha1.slice(0, 5); const suffix = sha1.slice(5);
const response = await fetch( `https://api.pwnedpasswords.com/range/${prefix}` );
const text = await response.text(); const hashes = text.split('\n').map(line => line.split(':'));
return hashes.some(([hash]) => hash === suffix);}
// При регистрацииasync function validatePassword(password) { if (password.length < 8) { throw new Error('Password too short'); }
const isPwned = await isPasswordPwned(password); if (isPwned) { throw new Error('This password has been found in data breaches. Choose another.'); }}Rate Limiting
Заголовок раздела «Rate Limiting»npm install express-rate-limitimport rateLimit from 'express-rate-limit';
// Ограничение для login endpointconst loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 минут max: 5, // 5 попыток standardHeaders: true, legacyHeaders: false, message: { error: 'Too many login attempts, try again in 15 minutes' },
// Блокируем по IP + email keyGenerator: (req) => `${req.ip}:${req.body.email}`,
// Не считаем успешные логины skipSuccessfulRequests: true,});
app.post('/login', loginLimiter, loginHandler);
// Глобальный rate limit для всего APIconst globalLimiter = rateLimit({ windowMs: 60 * 1000, // 1 минута max: 100, // 100 запросов});
app.use('/api/', globalLimiter);Чеклист: Сессии и токены
Заголовок раздела «Чеклист: Сессии и токены»☐ SESSION_SECRET длинный (64+ символов), случайный☐ JWT_SECRET длинный (64+ символов), случайный☐ Сессионные куки: HttpOnly + Secure + SameSite☐ Access токены короткоживущие (15 мин)☐ Refresh токены хранятся в HttpOnly cookie☐ Session ID регенерируется после логина☐ Logout уничтожает сессию на сервере☐ JWT содержат только необходимые данные (не пароли!)☐ JWT всегда имеют exp (срок истечения)Чеклист: HTTPS и заголовки
Заголовок раздела «Чеклист: HTTPS и заголовки»☐ HTTPS включён везде (редирект с HTTP)☐ HSTS заголовок настроен☐ CSP заголовок настроен☐ X-Content-Type-Options: nosniff☐ X-Frame-Options: DENY или SAMEORIGIN☐ Referrer-Policy настроен☐ CORS whitelist настроен (не *)☐ Проверка на securityheaders.com: A или A+Чеклист: Аутентификация и авторизация
Заголовок раздела «Чеклист: Аутентификация и авторизация»☐ Все endpoint'ы требуют аутентификации (кроме публичных)☐ Авторизация проверяется на сервере (не только на клиенте)☐ Пользователь видит только свои данные (data isolation)☐ RBAC/ABAC настроен☐ Admin роут отделён и дополнительно защищён☐ OAuth state параметр используется против CSRF☐ PKCE используется для публичных OAuth клиентовЧеклист: Защита от атак
Заголовок раздела «Чеклист: Защита от атак»☐ XSS: все пользовательские данные экранированы☐ XSS: Rich text санитизируется через DOMPurify☐ CSRF: SameSite куки + CSRF токены для форм☐ SQL Injection: используются Prepared Statements или ORM☐ Path Traversal: проверяются пути к файлам☐ SSRF: валидируются внешние URL в запросах☐ Open Redirect: whitelist для redirect URLОпасные паттерны которых нужно избегать
Заголовок раздела «Опасные паттерны которых нужно избегать»Open Redirect
Заголовок раздела «Open Redirect»// ОПАСНОapp.get('/redirect', (req, res) => { res.redirect(req.query.url); // злоумышленник может перенаправить на свой сайт});
// БЕЗОПАСНОconst allowedRedirects = ['/dashboard', '/profile', '/settings'];
app.get('/redirect', (req, res) => { const url = req.query.url;
if (allowedRedirects.includes(url)) { res.redirect(url); } else { res.redirect('/'); }});Mass Assignment
Заголовок раздела «Mass Assignment»// ОПАСНО: пользователь может передать role: 'admin'app.post('/profile', async (req, res) => { await db.users.update(userId, req.body); // опасно!});
// БЕЗОПАСНО: белый список полейconst { name, bio, avatar } = req.body;await db.users.update(userId, { name, bio, avatar }); // только разрешённые поляSSRF (Server-Side Request Forgery)
Заголовок раздела «SSRF (Server-Side Request Forgery)»// ОПАСНО: сервер делает запрос по URL от пользователяapp.post('/fetch-url', async (req, res) => { const data = await fetch(req.body.url); // может обратиться к внутренней сети! res.json(data);});
// БЕЗОПАСНО: валидация URLfunction isAllowedURL(url) { try { const parsed = new URL(url);
// Только HTTP/HTTPS if (!['http:', 'https:'].includes(parsed.protocol)) return false;
// Запрет внутренних адресов const blockedHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1']; if (blockedHosts.includes(parsed.hostname)) return false;
// Запрет RFC 1918 диапазонов (приватные IP) const privateRanges = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/; if (privateRanges.test(parsed.hostname)) return false;
return true; } catch { return false; }}Аудит и мониторинг
Заголовок раздела «Аудит и мониторинг»Логирование событий безопасности
Заголовок раздела «Логирование событий безопасности»import winston from 'winston';
const securityLogger = winston.createLogger({ transports: [new winston.transports.File({ filename: 'security.log' })],});
// Логируем важные событияfunction logSecurityEvent(event, details) { securityLogger.info({ event, timestamp: new Date().toISOString(), ...details, });}
// ПримерыlogSecurityEvent('LOGIN_SUCCESS', { userId, ip: req.ip });logSecurityEvent('LOGIN_FAILURE', { email, ip: req.ip, reason: 'invalid_password' });logSecurityEvent('PASSWORD_RESET', { userId, ip: req.ip });logSecurityEvent('ROLE_CHANGED', { userId, from: 'user', to: 'admin', changedBy: adminId });logSecurityEvent('SUSPICIOUS_ACTIVITY', { userId, ip: req.ip, action: 'multiple_failed_logins' });Мониторинг подозрительной активности
Заголовок раздела «Мониторинг подозрительной активности»// Детектирование необычной активностиasync function detectSuspiciousActivity(userId, ip) { const recentFailures = await redis.incr(`login_failures:${userId}`); await redis.expire(`login_failures:${userId}`, 3600); // 1 час
if (recentFailures >= 10) { // Временная блокировка await redis.setex(`user_blocked:${userId}`, 1800, '1'); // 30 минут
// Уведомление пользователя await sendEmail(user.email, 'Подозрительная активность', 'Ваш аккаунт временно заблокирован из-за множественных неудачных попыток входа.' );
// Алерт администратору logSecurityEvent('ACCOUNT_BLOCKED', { userId, ip, failures: recentFailures }); }}2FA (Two-Factor Authentication)
Заголовок раздела «2FA (Two-Factor Authentication)»TOTP (Google Authenticator совместимый)
Заголовок раздела «TOTP (Google Authenticator совместимый)»npm install speakeasy qrcodeimport speakeasy from 'speakeasy';import QRCode from 'qrcode';
// Генерация секрета для пользователяfunction generate2FASecret(email) { const secret = speakeasy.generateSecret({ name: `MyApp (${email})`, length: 20, });
return { secret: secret.base32, otpauthUrl: secret.otpauth_url, };}
// Генерация QR кодаasync function generate2FAQR(otpauthUrl) { return QRCode.toDataURL(otpauthUrl);}
// Верификация кодаfunction verify2FA(secret, token) { return speakeasy.totp.verify({ secret, encoding: 'base32', token, window: 1, // допуск ±30 секунд });}
// Роутыapp.post('/2fa/setup', authMiddleware, async (req, res) => { const { secret, otpauthUrl } = generate2FASecret(req.user.email);
// Временно сохраняем секрет (не активируем пока не подтверждён) await db.users.update(req.user.id, { twoFactorSecretTemp: encrypt(secret) });
const qrCode = await generate2FAQR(otpauthUrl); res.json({ qrCode, secret }); // показываем QR + backup код});
app.post('/2fa/verify', authMiddleware, async (req, res) => { const user = await db.users.findById(req.user.id); const secret = decrypt(user.twoFactorSecretTemp);
if (!verify2FA(secret, req.body.token)) { return res.status(400).json({ error: 'Invalid code' }); }
// Активируем 2FA await db.users.update(req.user.id, { twoFactorSecret: encrypt(secret), twoFactorEnabled: true, twoFactorSecretTemp: null, });
res.json({ success: true });});Dependency Security
Заголовок раздела «Dependency Security»# Проверка уязвимостей в зависимостяхnpm audit
# Автоматическое исправлениеnpm audit fix
# Регулярные обновленияnpx npm-check-updates -u && npm installИтоговый Security Score
Заголовок раздела «Итоговый Security Score»Оцени свой проект:
| Область | Баллы | Мой статус |
|---|---|---|
| HTTPS + HSTS | 10 | ☐ |
| Security Headers (A+) | 15 | ☐ |
| Bcrypt/Argon2 для паролей | 15 | ☐ |
| Rate Limiting | 10 | ☐ |
| XSS защита | 10 | ☐ |
| CSRF защита | 10 | ☐ |
| RBAC авторизация | 10 | ☐ |
| Аудит логи | 10 | ☐ |
| 2FA | 10 | ☐ |
| Итого | 100 |
80+ — отлично
60-79 — хорошо, есть над чем работать
< 60 — критические проблемы, нужно исправить немедленно
Дополнительные ресурсы
Заголовок раздела «Дополнительные ресурсы»- OWASP Top 10
- OWASP Cheat Sheet Series
- NodeJS Security Checklist
- Auth0 Security Blog
- HaveIBeenPwned
- SecurityHeaders.com