Стандарты безопасности
Покрытие OWASP Top 10
Чек-лист соответствия архитектуры требованиям OWASP Top 10 (2021):
| # | Уязвимость | Меры защиты | Статус |
|---|---|---|---|
| A01 | Нарушение контроля доступа | RBAC с guards, проверки на уровне строк, валидация JWT | ✅ Покрыто |
| A02 | Криптографические сбои | Шифрование AES-256-GCM, пароли bcrypt, обязательный TLS | ✅ Покрыто |
| A03 | Инъекции | Параметризованные запросы Prisma, валидация входных данных, без raw SQL | ✅ Покрыто |
| A04 | Небезопасный дизайн | Clean Architecture, моделирование угроз на этапе проектирования | ✅ Покрыто |
| A05 | Неправильная конфигурация безопасности | Заголовки Helmet, белый список CORS, валидация env | ✅ Покрыто |
| A06 | Уязвимые компоненты | npm audit в CI, оповещения Dependabot | ⚠️ Требуется настройка CI |
| A07 | Сбои аутентификации | JWT + refresh токены, rate limiting, блокировка аккаунта | ✅ Покрыто |
| A08 | Сбои целостности данных | Проверка подписи webhook, ключи идемпотентности | ✅ Покрыто |
| A09 | Сбои логирования | Структурированное логирование, аудит-трейлы, маскирование чувствительных данных | ✅ Покрыто |
| A10 | SSRF | Нет пользовательских URL в бэкенд-запросах | ✅ N/A по дизайну |
Сканирование зависимостей (A06)
yaml
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 0 * * 1' # Еженедельно
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --audit-level=high
- uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}Политика паролей
typescript
// Минимальные требования к паролю
const PASSWORD_POLICY = {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumber: true,
requireSpecialChar: false, // Опционально для лучшего UX
};
// Валидация с class-validator
@IsString()
@MinLength(8)
@MaxLength(128)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain uppercase, lowercase, and number',
})
password: string;Блокировка аккаунта
typescript
// После 5 неудачных попыток входа за 15 минут:
// - Блокировка аккаунта на 30 минут
// - Отправка уведомления по email
const LOCKOUT_POLICY = {
maxAttempts: 5,
windowMinutes: 15,
lockoutMinutes: 30,
notifyOnLockout: true,
};Конфигурация CORS
typescript
// main.ts
app.enableCors({
origin: configService.get('FRONTEND_URL'),
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
credentials: true,
maxAge: 86400, // 24 часа
});Заголовки безопасности (Helmet)
typescript
// main.ts
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Для Swagger UI
imgSrc: ["'self'", 'data:', 'https:'],
scriptSrc: ["'self'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
}));Санитизация входных данных
typescript
// Глобальное использование class-transformer и class-validator
// main.ts
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Удалять неизвестные свойства
forbidNonWhitelisted: true, // Ошибка при неизвестных свойствах
transform: true, // Автоматическое преобразование типов
transformOptions: {
enableImplicitConversion: true,
},
}));
// HTML санитизация для текстовых полей
import { sanitize } from 'class-sanitizer';
@Transform(({ value }) => sanitize(value))
@IsString()
description: string;Чувствительные данные в логах
typescript
// НИКОГДА не логировать:
// - Пароли
// - API ключи/токены
// - Полные номера карт
// - Полные KYC документы
// - Сессионные токены
// Санитизация перед логированием
const sanitizeForLogging = (obj: Record<string, unknown>): Record<string, unknown> => {
const sensitiveFields = ['password', 'token', 'apiKey', 'secret', 'cardNumber'];
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => {
if (sensitiveFields.some(f => key.toLowerCase().includes(f.toLowerCase()))) {
return [key, '[REDACTED]'];
}
return [key, value];
})
);
};Стратегия шифрования данных
Обзор
Платформа обрабатывает чувствительные данные, требующие шифрования:
- KYC документы: Сканы паспортов, фото ID
- Платёжные данные: Номера счетов для выплат
- Персональные данные: ИНН, номера телефонов (PII)
Подход к шифрованию
| Тип данных | Хранилище | Метод шифрования |
|---|---|---|
| Пароли | База данных | bcrypt (односторонний хэш) |
| Сессионные токены | База данных | SHA256 хэш |
| KYC документы | Объектное хранилище (S3) | AES-256-GCM + шифрованный bucket |
| Данные для выплат | База данных (JSONB) | AES-256-GCM на уровне приложения |
| ИНН | База данных | AES-256-GCM на уровне приложения |
Шифрование на уровне полей (pgcrypto)
Для чувствительных полей, хранящихся непосредственно в PostgreSQL:
sql
-- Включение расширения pgcrypto
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Пример: Хранение зашифрованного ИНН
CREATE TABLE core.tax_identifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES core.users(id),
tax_id_type VARCHAR(20) NOT NULL, -- SSN, TIN, ITIN
tax_id_encrypted BYTEA NOT NULL, -- Зашифровано с pgcrypto
tax_id_hash VARCHAR(64) NOT NULL, -- Для поиска (SHA256)
jurisdiction VARCHAR(3) NOT NULL, -- Код страны
verified_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Уникальный индекс по хэшу для предотвращения дубликатов
CREATE UNIQUE INDEX idx_tax_id_hash ON core.tax_identifications(tax_id_hash);Сервис шифрования на уровне приложения
typescript
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
import { promisify } from 'util';
const scryptAsync = promisify(scrypt);
export class EncryptionService {
private readonly algorithm = 'aes-256-gcm';
private readonly keyLength = 32;
private readonly ivLength = 16;
private readonly tagLength = 16;
constructor(private readonly masterKey: string) {}
async encrypt(plaintext: string): Promise<string> {
// Деривация ключа из мастер-ключа с помощью scrypt
const salt = randomBytes(16);
const key = (await scryptAsync(this.masterKey, salt, this.keyLength)) as Buffer;
// Генерация IV
const iv = randomBytes(this.ivLength);
// Шифрование
const cipher = createCipheriv(this.algorithm, key, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const tag = cipher.getAuthTag();
// Объединение: salt(16) + iv(16) + tag(16) + ciphertext
const combined = Buffer.concat([salt, iv, tag, encrypted]);
return combined.toString('base64');
}
async decrypt(ciphertext: string): Promise<string> {
const combined = Buffer.from(ciphertext, 'base64');
// Извлечение компонентов
const salt = combined.subarray(0, 16);
const iv = combined.subarray(16, 32);
const tag = combined.subarray(32, 48);
const encrypted = combined.subarray(48);
// Деривация ключа
const key = (await scryptAsync(this.masterKey, salt, this.keyLength)) as Buffer;
// Расшифровка
const decipher = createDecipheriv(this.algorithm, key, iv);
decipher.setAuthTag(tag);
return decipher.update(encrypted) + decipher.final('utf8');
}
hash(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
}Управление ключами
| Аспект | Реализация |
|---|---|
| Хранение мастер-ключа | Переменная окружения (AWS Secrets Manager в продакшене) |
| Ротация ключей | Поддержка версионированных ключей с постепенным перешифрованием |
| Доступ к ключам | Только сервисный слой приложения, никогда не экспонируется в API |
| Шифрование бэкапов | Отдельный ключ, хранится в другом месте |
Конфигурация шифрования
typescript
// config/encryption.schema.ts
export const encryptionConfigSchema = z.object({
ENCRYPTION_MASTER_KEY: z.string().min(32),
ENCRYPTION_KEY_VERSION: z.string().default('v1'),
KYC_BUCKET_ENCRYPTION_KEY_ARN: z.string().optional(),
});Что шифровать, а что хэшировать
| Данные | Метод | Причина |
|---|---|---|
| Пароль | Хэш (bcrypt) | Оригинал не нужен |
| ИНН | Шифрование + Хэш | Нужен оригинал для отображения, хэш для поиска |
| Счёт для выплат | Шифрование | Нужен оригинал для выплат |
| KYC документ | Шифрование (at rest) | Нужен оригинал для проверки |
| Сессионный токен | Хэш | Нужна только верификация |
| Отпечаток устройства | Хэш | Нужна только верификация |
Защита админских эндпоинтов
Белый список IP для админ-панели
Админские эндпоинты (/api/v1/admin/*) должны быть ограничены известными IP-адресами в продакшене.
typescript
// middleware/admin-ip-allowlist.middleware.ts
import { Injectable, NestMiddleware, ForbiddenException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AdminIpAllowlistMiddleware implements NestMiddleware {
private readonly allowedIps: string[];
constructor(private readonly config: ConfigService) {
// Загрузка из окружения: ADMIN_ALLOWED_IPS=1.2.3.4,5.6.7.8,10.0.0.0/8
const ipsConfig = this.config.get<string>('ADMIN_ALLOWED_IPS', '');
this.allowedIps = ipsConfig.split(',').filter(Boolean);
}
use(req: Request, res: Response, next: NextFunction) {
// Пропускать в разработке
if (this.config.get('NODE_ENV') === 'development') {
return next();
}
// Пропускать если IP не настроены (для постепенного внедрения)
if (this.allowedIps.length === 0) {
return next();
}
const clientIp = this.getClientIp(req);
if (!this.isIpAllowed(clientIp)) {
throw new ForbiddenException('Access denied: IP not in allowlist');
}
next();
}
private getClientIp(req: Request): string {
// Обработка прокси (X-Forwarded-For)
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) {
return (forwarded as string).split(',')[0].trim();
}
return req.ip || req.socket.remoteAddress || '';
}
private isIpAllowed(ip: string): boolean {
return this.allowedIps.some(allowed => {
if (allowed.includes('/')) {
// CIDR нотация (например, 10.0.0.0/8)
return this.isIpInCidr(ip, allowed);
}
return ip === allowed;
});
}
private isIpInCidr(ip: string, cidr: string): boolean {
// Реализация проверки CIDR
const [range, bits] = cidr.split('/');
const mask = ~(2 ** (32 - parseInt(bits)) - 1);
const ipNum = this.ipToNumber(ip);
const rangeNum = this.ipToNumber(range);
return (ipNum & mask) === (rangeNum & mask);
}
private ipToNumber(ip: string): number {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0;
}
}Применение к админским маршрутам
typescript
// app.module.ts
@Module({
imports: [...],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AdminIpAllowlistMiddleware)
.forRoutes({ path: 'api/v1/admin/*', method: RequestMethod.ALL });
}
}Конфигурация
bash
# .env.production
ADMIN_ALLOWED_IPS=203.0.113.50,198.51.100.0/24,10.0.0.0/8| Окружение | Рекомендация |
|---|---|
| Разработка | Отключено (пустой список) |
| Staging | IP офиса + диапазон VPN |
| Продакшен | Только IP офиса + диапазон VPN |
План аудита безопасности
Чек-лист перед запуском
| Проверка | Инструмент/Метод | Частота |
|---|---|---|
| Уязвимости зависимостей | npm audit, Snyk | Каждая сборка |
| Сканирование секретов | GitHub secret scanning, gitleaks | Каждый коммит |
| SAST (статический анализ) | SonarQube, CodeQL | Еженедельно |
| Сканирование контейнеров | Trivy, Docker Scout | Каждая сборка |
| Обзор инфраструктуры | Ручной чек-лист | Перед запуском |
Расписание после запуска
| Активность | Частота | Охват |
|---|---|---|
| Автоматическое сканирование уязвимостей | Еженедельно | Зависимости, контейнеры |
| Ручной обзор безопасности | Ежеквартально | Изменения кода с прошлого обзора |
| Тест на проникновение | Ежегодно | Полное приложение + инфраструктура |
| Аудит доступов | Ежемесячно | Админские аккаунты, API ключи, доступ к БД |
| Учения по реагированию на инциденты | Раз в полгода | Готовность команды |
Объём тестирования на проникновение
При привлечении пентестеров включить:
В объёме:
- Аутентификация и управление сессиями
- Авторизация и контроль доступа (обход RBAC)
- Манипуляция платёжным потоком
- Подделка расчёта комиссий
- Попытки манипуляции MLM-деревом
- Обход rate limiting API
- Попытки подделки webhook
- Доступ к админ-панели
Вне объёма:
- Физическая безопасность
- Социальная инженерия (если не согласовано)
- DDoS атаки
- Сторонние сервисы (Stripe, платёжные провайдеры)
Раскрытие уязвимостей
markdown
# SECURITY.md (в корне репозитория)
## Сообщение о проблемах безопасности
Пожалуйста, сообщайте об уязвимостях безопасности на: security@iwm-platform.com
НЕ создавайте публичные GitHub issues для уязвимостей безопасности.
Мы подтвердим получение в течение 48 часов и предоставим
подробный ответ в течение 7 дней.Метрики безопасности для отслеживания
| Метрика | Цель | Порог оповещения |
|---|---|---|
| Время исправления критических CVE | < 24 часа | > 48 часов |
| Неудачные попытки входа | Базовый уровень | > 5x от базового |
| Доступ админа с нового IP | 0 неожиданных | Любой неожиданный |
| Ошибки подписи webhook | < 0.1% | > 1% |
| Срабатывания rate limit | Базовый уровень | > 10x от базового |