# Security And Hardening

> Укрепляет код против уязвимостей. Используй при работе с пользовательским вводом, аутентификацией, хранением данных или внешними интеграциями. Используй при создании любой функциональности, которая принимает недоверенные данные, управляет пользовательскими сессиями или взаимодействует со сторонними сервисами.

- Skill: `aleksandr-litvinenko/security-and-hardening` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aleksandr-litvinenko/security-and-hardening`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aleksandr-litvinenko/security-and-hardening/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: Aleksandr-Litvinenko (https://skillmd.com/u/aleksandr-litvinenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aleksandr-litvinenko/security-and-hardening

---


# Безопасность и укрепление

## Обзор

Практики разработки, где безопасность на первом месте. Считай любой внешний ввод враждебным, любой секрет — священным, а любую проверку прав — обязательной. Безопасность не фаза, а ограничение на каждую строку кода, которая касается пользовательских данных, аутентификации или внешних систем.

## Когда применять

- Строишь что-либо, принимающее пользовательский ввод
- Реализуешь аутентификацию или авторизацию
- Хранишь или передаёшь чувствительные данные
- Интегрируешься с внешними API или сервисами
- Добавляешь загрузку файлов, вебхуки или колбэки
- Работаешь с платежами или персональными данными

## Процесс: сначала модель угроз

Меры защиты, прикрученные без модели угроз, — это догадки. Прежде чем укреплять, потрать пять минут на то, чтобы подумать как атакующий:

1. **Нанеси на карту границы доверия.** Где недоверенные данные попадают в твою систему? HTTP-запросы, поля форм, загрузки файлов, вебхуки, сторонние API, очереди сообщений и **вывод LLM**. Каждая граница — поверхность атаки.
2. **Назови активы.** Что стоит украсть или сломать? Учётные данные, персональные данные, платёжные данные, административные действия, движение денег.
3. **Прогони STRIDE по каждой границе** — быстрая линза, а не ритуал:

| Угроза | Спроси | Типовая мера |
|---|---|---|
| **S**poofing (подмена) | Может ли кто-то выдать себя за пользователя/сервис? | Аутентификация, проверка подписи |
| **T**ampering (искажение) | Могут ли данные быть изменены при передаче или хранении? | Проверки целостности, параметризованные запросы, HTTPS |
| **R**epudiation (отказ от действия) | Можно ли потом отрицать действие? | Аудит-логирование событий безопасности |
| **I**nformation disclosure (раскрытие) | Могут ли данные утечь? | Шифрование, белые списки полей, обобщённые ошибки |
| **D**enial of service (отказ в обслуживании) | Можно ли перегрузить? | Ограничение частоты, лимиты размера ввода, таймауты |
| **E**levation of privilege (повышение прав) | Может ли пользователь получить права, которых не должен иметь? | Проверки авторизации, минимум привилегий |

4. **Пиши сценарии злоупотребления рядом со сценариями использования.** Для каждой функции спроси: «как бы я это использовал во вред?» — и сделай это своим первым тестом.

Если ты не можешь назвать границы доверия для функции, ты не готов её защищать. Это OWASP **A04: небезопасное проектирование** — большинство утечек начинается в проектировании, а не в коде.

## Трёхуровневая система границ

### Всегда делать (без исключений)

- **Валидировать весь внешний ввод** на границе системы (маршруты API, обработчики форм)
- **Параметризовать все запросы к БД** — никогда не склеивать пользовательский ввод в SQL
- **Кодировать вывод** для предотвращения XSS (использовать автоэкранирование фреймворка и не обходить его)
- **Использовать HTTPS** для всей внешней связи
- **Хешировать пароли** через bcrypt/scrypt/argon2 (никогда не хранить открытым текстом)
- **Устанавливать заголовки безопасности** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- **Использовать куки с httpOnly, secure, sameSite** для сессий
- **Запускать штатный аудит обнаруженного пакетного менеджера** по закоммиченному lock-файлу перед каждым релизом

### Сначала спросить (нужно одобрение человека)

- Добавление новых потоков аутентификации или изменение логики авторизации
- Хранение новых категорий чувствительных данных (персональные данные, платёжная информация)
- Добавление интеграций с новыми внешними сервисами
- Изменение конфигурации CORS
- Добавление обработчиков загрузки файлов
- Изменение ограничения частоты запросов
- Выдача повышенных прав или ролей

### Никогда не делать

- **Никогда не коммитить секреты** в систему контроля версий (ключи API, пароли, токены)
- **Никогда не логировать чувствительные данные** (пароли, токены, полные номера карт)
- **Никогда не считать клиентскую валидацию** границей безопасности
- **Никогда не отключать заголовки безопасности** ради удобства
- **Никогда не использовать `eval()` или `innerHTML`** с данными от пользователя
- **Никогда не хранить сессии в доступном клиенту хранилище** (localStorage для токенов авторизации)
- **Никогда не показывать стек-трейсы** и внутренние детали ошибок пользователям

## Паттерны предотвращения из OWASP Top 10

Это паттерны предотвращения, а не ранжирование. Порядок редакции 2021 года см. в справочной таблице `../../references/security-checklist.md`.

### Инъекции (SQL, NoSQL, команды ОС)

```typescript
// ПЛОХО: SQL-инъекция через склейку строк
const query = `SELECT * FROM users WHERE id = '${userId}'`;

// ХОРОШО: параметризованный запрос
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

// ХОРОШО: ORM с параметризованным вводом
const user = await prisma.user.findUnique({ where: { id: userId } });
```

### Сломанная аутентификация

```typescript
// Хеширование паролей
import { hash, compare } from 'bcrypt';

const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);

// Управление сессиями
app.use(session({
  secret: process.env.SESSION_SECRET,  // Из окружения, а не из кода
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,     // Недоступна из JavaScript
    secure: true,       // Только по HTTPS
    sameSite: 'lax',    // Защита от CSRF
    maxAge: 24 * 60 * 60 * 1000,  // 24 часа
  },
}));
```

### Межсайтовый скриптинг (XSS)

```typescript
// ПЛОХО: отрисовка пользовательского ввода как HTML
element.innerHTML = userInput;

// ХОРОШО: использовать автоэкранирование фреймворка (React делает это по умолчанию)
return <div>{userInput}</div>;

// Если HTML отрисовывать ОБЯЗАТЕЛЬНО, сначала очисти
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
```

### Сломанный контроль доступа

```typescript
// Всегда проверяй авторизацию, а не только аутентификацию
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
  const task = await taskService.findById(req.params.id);

  // Проверить, что аутентифицированный пользователь владеет этим ресурсом
  if (task.ownerId !== req.user.id) {
    return res.status(403).json({
      error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' }
    });
  }

  // Продолжить обновление
  const updated = await taskService.update(req.params.id, req.body);
  return res.json(updated);
});
```

### Небезопасная конфигурация

```typescript
// Заголовки безопасности (для Express используй helmet)
import helmet from 'helmet';
app.use(helmet());

// Политика безопасности содержимого
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],  // По возможности ужесточить
    imgSrc: ["'self'", 'data:', 'https:'],
    connectSrc: ["'self'"],
  },
}));

// CORS — ограничить известными источниками
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
}));
```

### Раскрытие чувствительных данных

```typescript
// Никогда не возвращай чувствительные поля в ответах API
function sanitizeUser(user: UserRecord): PublicUser {
  const { passwordHash, resetToken, ...publicFields } = user;
  return publicFields;
}

// Используй переменные окружения для секретов
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');
```

### Подделка запросов на стороне сервера (SSRF)

Каждый раз, когда сервер загружает URL, на который повлиял пользователь — вебхуки, «импорт по ссылке», прокси изображений, превью ссылок, — атакующий может направить его на внутренние сервисы (метаданные облака, `localhost`, приватные адреса).

```typescript
// ПЛОХО: загружаем всё, что дал пользователь
await fetch(req.body.webhookUrl);

// ХОРОШО: белый список схемы и хоста, отказ, если ЛЮБОЙ разрешённый IP приватный, запрет перенаправлений
import { lookup } from 'node:dns/promises';
import ipaddr from 'ipaddr.js';

const ALLOWED_HOSTS = new Set(['hooks.example.com']);

async function assertSafeUrl(raw: string): Promise<URL> {
  const url = new URL(raw);
  if (url.protocol !== 'https:') throw new Error('https only');
  if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('host not allowed');
  // Разрешаем ВСЕ записи; один приватный/зарезервированный адрес проваливает проверку.
  const addrs = await lookup(url.hostname, { all: true });
  if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) {
    throw new Error('private/reserved IP');
  }
  return url;
}

await fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' });
```

Проверка `range() !== 'unicast'` покрывает петлевые адреса, link-local `169.254.169.254` (метаданные облака, цель SSRF номер один), приватные и unique-local диапазоны в IPv4 и IPv6.

**Оговорка — здесь остаётся разрыв TOCTOU.** `fetch` заново разрешает DNS после проверки, поэтому атакующий с записью с коротким TTL может переключиться на внутренний адрес между валидацией и соединением. Для поверхностей с высоким риском разреши имя один раз и соединяйся с зафиксированным адресом либо поставь спереди фильтрующий агент (`request-filtering-agent` / `ssrf-req-filter`).

## Паттерны валидации ввода

### Валидация по схеме на границах

```typescript
import { z } from 'zod';

const CreateTaskSchema = z.object({
  title: z.string().min(1).max(200).trim(),
  description: z.string().max(2000).optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
  dueDate: z.string().datetime().optional(),
});

// Валидация в обработчике маршрута
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid input',
        details: result.error.flatten(),
      },
    });
  }
  // result.data теперь типизирован и провалидирован
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});
```

### Безопасная загрузка файлов

```typescript
// Ограничивай типы и размеры файлов
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5 МБ

function validateUpload(file: UploadedFile) {
  if (!ALLOWED_TYPES.includes(file.mimetype)) {
    throw new ValidationError('File type not allowed');
  }
  if (file.size > MAX_SIZE) {
    throw new ValidationError('File too large (max 5MB)');
  }
  // Не доверяй расширению файла — если это критично, проверяй сигнатуру байтов
}
```

## Разбор результатов аудита зависимостей

Аудиты пакетных менеджеров сообщают об известных уязвимостях; они не доказывают, что пакет заслуживает доверия, и что уязвимый код достижим. Используй это дерево решений:

```
Штатный аудит пакетного менеджера сообщает об уязвимости
├── Серьёзность: критичная или высокая
│   ├── Достижим ли уязвимый код в путях рантайма, сборки, тестов или выкатки?
│   │   ├── ДА → чинить немедленно (обновить, пропатчить или заменить зависимость)
│   │   └── НЕТ (подтверждено, что не используется ни в одном из этих путей) → чинить скоро, но это не блокер
│   └── Есть ли исправление?
│       ├── ДА → обновиться до пропатченной версии
│       └── НЕТ → искать обходные пути, рассмотреть замену зависимости или внести в белый список с датой пересмотра
├── Серьёзность: средняя
│   ├── Достижимо в продакшне? → чинить в следующем релизном цикле
│   └── Только для разработки? → чинить, когда удобно, вести в бэклоге
└── Серьёзность: низкая
    └── Отслеживать и чинить при плановых обновлениях зависимостей
```

**Ключевые вопросы:**
- Вызывается ли уязвимая функция в твоём пути выполнения?
- Это зависимость рантайма или только для разработки?
- Эксплуатируема ли уязвимость в твоём контексте выкатки (например, серверная уязвимость в чисто клиентском приложении)?

Когда откладываешь исправление, задокументируй причину и назначь дату пересмотра.

### Гигиена цепочки поставок

Не предполагай, что это npm, и не считай ближайший манифест корнем установки. Действуй в таком порядке:

1. **Найди границу установки и менеджер.** Используй корень рабочего пространства, которому принадлежит lock-файл, либо независимый вложенный проект — но только если он вне этого рабочего пространства. Там сверь `packageManager` (если есть), lock-файл и CI; останавливайся при расхождении или конкурирующих lock-файлах. Зафиксируй версию менеджера и используй таблицу соответствий из `../../references/security-checklist.md`.
2. **Блокируй скрипты зависимостей до первого выполнения.** Разворачивай проект с отключёнными скриптами или задокументированной политикой отказа по умолчанию, изучи исходники ожидающих скриптов, одобри минимально необходимые пакеты, закоммить политику, затем проверь чистой замороженной/неизменяемой установкой. Никогда не одобряй скрипты оптом.

Аудиты находят только известные уязвимости; они не поймают свежий вредоносный или тайпсквоттинговый пакет. Поэтому:

- **Никогда не применяй принудительное автоисправление аудита** (`npm audit fix --force` и аналоги). Посмотри, что предлагается, прочитай changelog и протестируй каждое получившееся обновление; принудительные исправления могут выйти за объявленные диапазоны версий.
- **Проверяй подписи и происхождение в реестре, где это поддерживается** (`npm audit signatures`, `pnpm audit signatures`), и считай их отсутствие поводом разобраться, а не автоматическим доказательством компрометации.
- **Рассматривай новые зависимости, дифф lock-файла и изменения политики скриптов вместе** — владение, сопровождение, возраст релиза, происхождение, транзитивный граф и тайпсквоттинг вроде `cross-env` против `crossenv` (OWASP **A06**, **LLM03**).

## Ограничение частоты запросов

```typescript
import rateLimit from 'express-rate-limit';

// Общий лимит для API
app.use('/api/', rateLimit({
  windowMs: 15 * 60 * 1000, // 15 минут
  max: 100,                   // 100 запросов за окно
  standardHeaders: true,
  legacyHeaders: false,
}));

// Более строгий лимит для эндпоинтов аутентификации
app.use('/api/auth/', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,  // 10 попыток за 15 минут
}));
```

## Управление секретами

```
Файлы .env:
  ├── .env.example  → Коммитится (шаблон со значениями-заглушками)
  ├── .env          → НЕ коммитится (содержит настоящие секреты)
  └── .env.local    → НЕ коммитится (локальные переопределения)

В .gitignore обязательно должны быть:
  .env
  .env.local
  .env.*.local
  *.pem
  *.key
```

**Всегда проверяй перед коммитом:**
```bash
# Проверить, не попали ли секреты в индекс случайно
git diff --cached | grep -i "password\|secret\|api_key\|token"
```

**Если секрет когда-либо попал в коммит — меняй его.** Удалить строку или переписать историю недостаточно: считай его скомпрометированным с момента попадания на удалённый сервер. Сначала отзови и перевыпусти ключ, потом вычищай его из истории.

## Защита функциональности с ИИ / LLM

Если твоё приложение вызывает LLM — чат-боты, суммаризаторы, агенты, RAG, — оно наследует новую поверхность атаки. Соотнеси её с [OWASP Top 10 для LLM-приложений (2025)](https://genai.owasp.org/llm-top-10/):

- **Считай любой вывод модели недоверенным вводом (LLM05: неправильная обработка вывода).** Никогда не передавай вывод LLM напрямую в `eval`, SQL, оболочку, `innerHTML` или путь к файлу. Валидируй и кодируй его ровно так же, как сырой пользовательский ввод.
- **Исходи из того, что промпты можно перехватить (LLM01: инъекция промпта).** Недоверенный текст в окне контекста — сообщение пользователя, загруженная веб-страница, PDF — может нести инструкции. Системный промпт не является границей безопасности; обеспечивай права в коде, а не в промпте.
- **Держи секреты и данные других пользователей вне промптов (LLM02 / LLM07).** Всё, что попало в контекст, может быть выдано обратно. Не помещай ключи API, данные других арендаторов и системный промпт целиком туда, где модель может это повторить.
- **Ограничивай права инструментов и агентов (LLM06: избыточная агентность).** Ограничивай инструменты минимумом, требуй подтверждения для разрушительных и необратимых действий и валидируй каждый аргумент инструмента.
- **Ограничивай потребление (LLM10: неограниченное потребление).** Ограничивай токены, частоту запросов и глубину циклов/рекурсии, чтобы специально составленный ввод не мог накрутить расходы или подвесить систему.
- **Изолируй данные поиска (LLM08: слабости векторов и эмбеддингов).** В RAG считай векторное хранилище границей доверия: разделяй эмбеддинги по арендаторам, чтобы один пользователь не мог достать данные другого, и валидируй документы до индексации, чтобы отравленное содержимое не могло направлять ответы.

```typescript
// ПЛОХО: доверять выводу модели как команде или как разметке
const sql = await llm.generate(`Write SQL for: ${userQuestion}`);
await db.query(sql);                                   // выполнение произвольного запроса
container.innerHTML = await llm.reply(userMessage);   // хранимый XSS, через модель

// ХОРОШО: вывод модели — это данные: защитно разобрать, потом провалидировать, потом закодировать
let intent;
try {
  intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage)));
} catch {
  throw new ValidationError('unexpected model output'); // JSON.parse или схема не прошли
}
await runAllowlistedAction(intent.action, intent.params);
container.textContent = await llm.reply(userMessage);
```

## Чеклист ревью безопасности

```markdown
### Аутентификация
- [ ] Пароли хешируются через bcrypt/scrypt/argon2 (раундов соли ≥ 12)
- [ ] Токены сессий с httpOnly, secure, sameSite
- [ ] На входе есть ограничение частоты запросов
- [ ] Токены сброса пароля истекают

### Авторизация
- [ ] Каждый эндпоинт проверяет права пользователя
- [ ] Пользователи имеют доступ только к своим ресурсам
- [ ] Административные действия требуют проверки роли администратора

### Ввод
- [ ] Весь пользовательский ввод валидируется на границе
- [ ] SQL-запросы параметризованы
- [ ] HTML-вывод кодируется/экранируется
- [ ] Серверные загрузки по URL идут по белому списку (нет SSRF к внутренним сервисам)

### Данные
- [ ] Секретов нет ни в коде, ни в системе контроля версий
- [ ] Чувствительные поля исключены из ответов API
- [ ] Персональные данные шифруются при хранении (если применимо)

### Инфраструктура
- [ ] Настроены заголовки безопасности (CSP, HSTS и прочие)
- [ ] CORS ограничен известными источниками
- [ ] Зависимости проверены на уязвимости
- [ ] Сообщения об ошибках не раскрывают внутренности

### Цепочка поставок
- [ ] Закоммичен один авторитетный lock-файл; CI использует замороженную/неизменяемую установку этого менеджера
- [ ] Находки штатного аудита разобраны по достижимости и риску исправления; скрипты установки зависимостей заблокированы, если не одобрены явно
- [ ] Новые зависимости проверены (владение, происхождение, возраст релиза, транзитивный граф)

### ИИ / LLM (если используется)
- [ ] Вывод модели считается недоверенным (нет eval/SQL/innerHTML/оболочки)
- [ ] Секреты и данные других пользователей не попадают в промпты
- [ ] Права инструментов/агентов ограничены; разрушительные действия требуют подтверждения
```
## См. также

Подробные чеклисты безопасности и шаги проверки перед коммитом — см. `../../references/security-checklist.md`.

## Типовые самооправдания

| Самооправдание | Как на самом деле |
|---|---|
| «Это внутренний инструмент, безопасность не важна» | Внутренние инструменты тоже компрометируют. Атакующие целятся в слабое звено. |
| «Добавим безопасность потом» | Прикрутить безопасность задним числом в 10 раз труднее, чем заложить её сразу. Добавляй сейчас. |
| «Никто не станет это эксплуатировать» | Автоматические сканеры найдут. Безопасность через неизвестность — не безопасность. |
| «Фреймворк заботится о безопасности» | Фреймворки дают инструменты, а не гарантии. Их всё равно нужно правильно применять. |
| «Это же просто прототип» | Прототипы становятся продакшном. Привычки безопасности с первого дня. |
| «Моделирование угроз здесь избыточно» | Пять минут вопроса «как бы я это атаковал?» предотвращают проектные дефекты, которые потом не залатать никакими мерами. |
| «Это же вывод LLM, просто текст» | Этот «текст» может быть SQL-запросом, тегом script или командой оболочки. Относись к нему как к любому недоверенному вводу. |
| «Аудит прошёл, значит зависимость безопасна» | Аудиты сопоставляют с известными уязвимостями. Они не обнаруживают свежий вредоносный пакет и не делают непроверенные скрипты установки безопасными для выполнения. |

## Тревожные признаки

- Пользовательский ввод передаётся напрямую в запросы к БД, команды оболочки или отрисовку HTML
- Секреты в исходном коде или истории коммитов
- Эндпоинты API без проверок аутентификации и авторизации
- Отсутствующая конфигурация CORS или подстановочный символ (`*`) в источниках
- Нет ограничения частоты запросов на эндпоинтах аутентификации
- Стек-трейсы или внутренние ошибки показываются пользователям
- Зависимости с известными критичными уязвимостями, конкурирующие lock-файлы на одной границе установки, невоспроизводимые установки или скрипты, одобренные оптом
- Сервер загружает пользовательские URL без белого списка (SSRF)
- Вывод LLM/модели передаётся в запрос, в DOM, в оболочку или в `eval`
- Секреты, персональные данные или системный промпт целиком помещены в окно контекста LLM

## Проверка

После реализации кода, значимого для безопасности:

- [ ] У штатного аудита нет неустранённых достижимых находок критичной/высокой серьёзности; CI сохраняет авторитетный lock-файл и блокирует непроверенные скрипты зависимостей
- [ ] Секретов нет ни в исходном коде, ни в истории git
- [ ] Весь пользовательский ввод валидируется на границах системы
- [ ] Аутентификация и авторизация проверяются на каждом защищённом эндпоинте
- [ ] Заголовки безопасности присутствуют в ответе (проверь через DevTools браузера)
- [ ] Ответы с ошибками не раскрывают внутренних деталей
- [ ] Ограничение частоты запросов активно на эндпоинтах аутентификации
- [ ] Серверные загрузки по URL проверяются по белому списку (нет SSRF)
- [ ] Вывод LLM/модели валидируется и кодируется перед использованием (если есть функциональность с ИИ)

