Skip to content

Стандарты безопасности

Покрытие 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Сбои логированияСтруктурированное логирование, аудит-трейлы, маскирование чувствительных данных✅ Покрыто
A10SSRFНет пользовательских 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
ОкружениеРекомендация
РазработкаОтключено (пустой список)
StagingIP офиса + диапазон 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 от базового
Доступ админа с нового IP0 неожиданныхЛюбой неожиданный
Ошибки подписи webhook< 0.1%> 1%
Срабатывания rate limitБазовый уровень> 10x от базового

Связанные документы