Принципы безопасного проектирования
Шесть принципов, на которые опирается всё остальное в курсе.
Защита в глубину (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, недоверие вводу, минимизация поверхности — фундамент.
- Рисуйте границы доверия и проверяйте данные на каждой из них.