Принципы безопасного проектирования

Шесть принципов, на которые опирается всё остальное в курсе.

Защита в глубину (defense in depth) — несколько независимых рубежей защиты, чтобы пробитие одного не означало компрометацию всей системы.

Зачем нужны принципы

Конкретные приёмы (параметризованные запросы, экранирование вывода, хеширование паролей) — это инструменты. Принципы — это причины, по которым вы выбираете тот или иной инструмент. Зная принципы, вы примете верное решение даже для технологии, о которой мы не говорили.

Технологии меняются стремительно: фреймворки устаревают, появляются новые протоколы, базы данных, способы деплоя. Если вы запомнили только список «делай так в этом фреймворке», то при переходе на следующий окажетесь беспомощны. Принципы переживают конкретику. Тот, кто понимает почему ввод не доверяют, сам сообразит, где в новой среде проходит граница и что на ней надо проверять, даже без готового рецепта. Поэтому принципы стоит усвоить как способ мышления, а не как набор правил для заучивания.

Полезно и то, что принципы помогают спорить и принимать решения в команде. Когда возникает вопрос «а нужна ли тут ещё одна проверка, ведь выше уже есть», ответ даёт принцип защиты в глубину. Когда кто-то предлагает выдать сервису широкие права «чтобы потом не возиться», least privilege подсказывает, чем это грозит. Принципы превращают расплывчатое «мне кажется, так безопаснее» в аргумент, который можно объяснить и проверить.

Шесть фундаментальных принципов

1. Минимум привилегий (least privilege)

Каждый компонент, пользователь и процесс получает ровно те права, которые нужны для работы — и ни на грамм больше. Сервис, который только читает таблицу, не должен иметь права DROP TABLE. Контейнер не должен бежать от root.

-- Уязвимо: приложение ходит в БД под суперпользователем
-- (взлом приложения = полный доступ к серверу БД)

-- Безопасно: отдельная роль с минимумом прав
CREATE USER app_ro WITH PASSWORD '...';
GRANT SELECT ON orders TO app_ro;        -- только чтение нужной таблицы
-- никаких GRANT ALL, никакого суперпользователя

2. Защита в глубину

Не полагайтесь на единственный барьер. Валидация на клиенте и на сервере; WAF и параметризованные запросы; шифрование канала и шифрование данных. Если один контроль обойдут, следующий ещё держит оборону.

Хорошая аналогия — средневековый замок: ров, стена, ворота, внутренний двор, башня. Нападающий, преодолевший ров, упирается в стену; пробив ворота, оказывается под обстрелом во дворе. Ни один рубеж не считается неприступным, и именно поэтому их несколько. В софте это значит, что вы исходите из допущения «каждый отдельный контроль когда-нибудь подведёт» и заранее ставите за ним следующий. Параметризованный запрос может быть обойдён из-за ошибки в одном месте — но если приложение всё равно ходит в базу под ролью с минимумом прав, ущерб ограничен. Слои не отменяют друг друга, а страхуют.

3. Fail securely (безопасный отказ)

Когда что-то идёт не так, система должна падать в закрытое, а не в открытое состояние. Ошибка проверки прав — значит «доступ запрещён», а не «пропустим на всякий случай».

// Уязвимо: при ошибке считаем, что доступ есть
let allowed = true;
try {
  allowed = checkPermission(user, resource);
} catch (e) {
  // забыли обработать -> allowed остаётся true -> доступ открыт
}

// Безопасно: по умолчанию запрещено, разрешаем только при явном "да"
let allowed = false;
try {
  allowed = checkPermission(user, resource) === true;
} catch (e) {
  allowed = false; // любой сбой -> отказ
}

4. Не доверяй вводу

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

5. Минимизация поверхности атаки

Чем меньше «дверей», тем меньше можно взломать. Выключайте неиспользуемые эндпоинты, порты, фичи и аккаунты. Не публикуйте отладочные панели в прод. Каждый лишний параметр — потенциальный вектор.

Поверхность атаки коварно разрастается сама собой. Включили библиотеку ради одной функции — получили весь её код и все её уязвимости. Оставили после демо тестовый эндпоинт — он живёт в продакшене годами, пока кто-нибудь его не найдёт. Завели сервисный аккаунт «на всякий случай» — и забыли отозвать. Минимизация поверхности — это не разовая чистка, а привычка спрашивать на каждом шаге: «нужно ли это здесь и сейчас, и что я смогу убрать, когда надобность отпадёт?». То, чего нет в системе, нельзя сломать.

6. Разделение обязанностей и простота

Сложность — враг безопасности: в запутанном коде легче спрятаться ошибке. Простые, читаемые проверки надёжнее «умных» однострочников. Разделяйте роли, чтобы одна скомпрометированная часть не давала всего.

Как работает под капотом: модель «доверия»

Полезно мысленно рисовать границы доверия (trust boundaries) — линии, которые пересекают данные, переходя из менее доверенной зоны в более доверенную (браузер → сервер, сервер → БД). На каждой такой границе данные надо проверять и/или преобразовывать.

Внутри одной зоны доверия мы условились данным верить; пересекая границу, эту веру нужно заслужить заново. Отсюда вытекает практический вывод: проверки имеют смысл именно на границах, а не разбросанные хаотично по коду. Данные, пришедшие из браузера, валидируются на входе в сервер; данные, уходящие в SQL, параметризуются на границе с БД; данные, отдаваемые обратно в HTML, экранируются на границе с браузером. Когда вы привыкаете видеть эти линии, становится очевидно, где забыта проверка: достаточно найти границу, через которую данные проходят «как есть».

Принципы не существуют по отдельности — они усиливают друг друга. Deny by default — это least privilege, применённый к доступу. Fail securely — это тот же least privilege в момент сбоя: раз мы не уверены, что доступ разрешён, по умолчанию его нет. Защита в глубину опирается на недоверие вводу, повторяя проверку на каждой границе. Поэтому правильнее думать о них не как о шести отдельных пунктах, а как о связной картине безопасного мышления.

 [ Браузер ]  --(не доверяем)-->  [ Сервер ]  --(параметризуем)-->  [ БД ]
   ввод юзера     валидация           бизнес-логика    запросы
                  + авторизация

Частые ошибки

  • «Проверили на клиенте — хватит». Клиент под контролем атакующего; серверная проверка обязательна.
  • Один большой админ-аккаунт на всё. Нарушает least privilege: его взлом отдаёт всё сразу.
  • Fail open в обработке исключений. Незамеченное исключение не должно открывать доступ.
  • Доверие к «внутренним» вызовам. Запрос от соседнего сервиса — это всё ещё запрос извне зоны; межсервисные границы тоже требуют аутентификации и проверки.
  • Чрезмерная «умность» проверок. Хитрый однострочник, который никто в команде не может уверенно прочитать, — это спрятанная ошибка; простая явная проверка надёжнее.

Итоги

  • Принципы объясняют почему мы выбираем конкретные приёмы.
  • Least privilege, defense in depth, fail securely, недоверие вводу, минимизация поверхности — фундамент.
  • Рисуйте границы доверия и проверяйте данные на каждой из них.
Проверьте себя
1. Что предписывает принцип «fail securely»?
AПри ошибке открывать доступ, чтобы не мешать пользователю
BПри сбое или ошибке переходить в закрытое (запрещающее) состояние по умолчанию
CНикогда не показывать сообщения об ошибках
DПерезапускать сервер при любой ошибке
2. Почему валидацию на клиенте нельзя считать достаточной?
AОна слишком медленная
BКлиент полностью под контролем атакующего, поэтому проверки можно обойти; нужна и серверная валидация
CJavaScript не умеет валидировать
DЭто нарушает приватность
3. Как принцип минимума привилегий применяется к доступу приложения в БД?
AПриложение работает под суперпользователем для удобства
BПриложение получает отдельную роль с правами только на нужные операции и таблицы
CВсе приложения используют один общий root-аккаунт
DПрава выдаются по запросу во время выполнения