О чем материал
Разбираемся, как повысить безопасность приложений через повседневные практики — без глубокого погружения в AppSec
Когда речь заходит о безопасности приложений, разработчикам часто предлагают «научиться думать как хакер»: изучить техники эксплуатации, освоить инструменты пентеста, разобраться в деревьях атак. Идея выглядит логичной: чтобы защититься, нужно понять, как атакуют. Но на практике она работает плохо, потому что игнорирует фундаментальную разницу между двумя типами мышления.
Хакер ищет одну брешь — разработчик должен не допустить ни одной. Хакер исследует систему без дедлайнов и ограничений — разработчик решает задачи в рамках спринта и работает в команде. Хакер мыслит «вне коробки», а разработчик — «внутри», но в этом заключается его сила: именно системный подход и следование паттернам позволяют строить надежное ПО.
Разработчикам следует относиться к уязвимостям как к багам, только особенным. Разница в том, что классические баги, как правило, мешают штатной функциональности, а уязвимости, наоборот, добавляют в приложение нештатную функциональность. С позиции разработчика нет смысла относиться к этому иначе. Если считать безопасность метрикой качества (а сегодня это так), то для ее повышения не нужно становиться экспертом по AppSec — достаточно просто хорошо делать свою работу. Писать код строго в рамках реализуемых фич, ограничивать входные и выходные данные, оценивать изменения на необходимость/достаточность и покрывать их соответствующими тестами. Большинство уязвимостей возникают не потому, что разработчик не знал, как устроена та же небезопасная десериализация, а потому, что добавил в код «фичу», которая позволяет десериализовать произвольные типы, но при этом не требуется для решения задачи.
OWASP Top 10 — общепризнанный хит-парад критичных рисков безопасности веб-приложений. Обычно его преподносят как каталог атак, с которого аппсекам стоит начинать работу над безопасностью проекта. Но каждый из десяти пунктов этого топа можно представить в виде простых правил кодинга, понятных любому разработчику. Давайте попробуем!
Сейчас я публикую в своем telegram-канале цикл материалов про ИБ для разработчиков — там еще больше полезного
- Контроль доступа: не давай больше, чем нужно
Вместо изучения способов подмены идентификатора ресурса в URL, достаточно следовать простому принципу: каждая операция с данными должна проверять, что текущий пользователь имеет право именно на эту операцию именно с этим ресурсом. На практике это означает:
- По умолчанию запрещай доступ ко всему, что не является публичным.
- Реализуй логику авторизации один раз и переиспользуй, а не разбрасывай проверки по контроллерам.
- Привязывай каждый набор данных к владельцу и проверяй принадлежность при любом обращении.
- Покрывай правила доступа тестами — как обычную функциональность, без мыслей о защите от атак.
- Авторизация — это бизнес-логика. Если она реализована полно и корректно, атакующему просто нечего будет эксплуатировать.
- Данные: защищай то, что не должно быть публичным
Речь не о криптографии, а об элементарной гигиене обращения с данными. Правило такое: определи, какие данные чувствительны, и обращайся с ними соответствующим образом.
- Не храни то, что не нужно хранить, — удаляй или анонимизируй данные сразу, как только они выполнили свою задачу.
- Пароли храни только в виде хэша с использованием современного алгоритма (bcrypt, Argon2) — никогда в открытом виде и никогда через MD5/SHA-256.
- Используй TLS везде, где передаются данные между клиентом и сервером.
- Не складывай секреты (ключи API, пароли баз данных, токены) в исходный код или конфигурационные файлы, которые попадают в репозиторий.
- Для создания токенов, ключей, векторов инициализации и соли используй криптографически стойкие генераторы случайных чисел — они есть в стандартных библиотеках любого языка.
- Входные данные: доверяй только тому, что сам определил
Разработчику не нужно знать десятки способов обхода WAF или отличия SQL-инъекции от инъекций команд ОС, но важно запомнить два правила:
- Никогда не конструируй на основе сырых входных данных то, что станет исполняемым кодом (SQL, HTML, команды ОС, LDAP-запросы) или описателем объекта (IP-адреса и доменные имена, файловые пути и URL, идентификаторы в хранилищах).
- Чтобы данные перестали быть сырыми, на входе используй валидацию (синтаксическая, семантическая) и/или типизацию, а на выходе — санитизацию (очистка, экранирование).
Как их применять:
- Для вывода данных наружу используй контекстно зависимую санитизацию: смотри не только куда попадают данные, но и в каком грамматическом контексте они окажутся.
- Помни, что санитизация не отменяет валидации/типизации и наоборот. Это разные механизмы, решающие разные задачи.
- Определяй допустимые множества для всех входных данных (белый список допустимых символов или regex-шаблон, длину, формат) и валидируй ввод на стороне сервера. Суть в том, чтобы входные данные всегда оставались именно данными и никогда не становились кодом (даже когда попадают внутрь другого кода).
- Архитектура: думай о границах доверия на этапе проектирования
Небезопасный дизайн — это когда проблемы нельзя исправить на этапе реализации, потому что они изначально заложены в архитектуру. Как этого избежать:
- При проектировании фичи явно определяй, кто может совершать определенные действия и с какими ограничениями.
- Не полагайся на то, что пользовательский интерфейс не позволит выполнить недопустимое действие, — валидируй ограничения на сервере.
- Устанавливай лимиты на потребление ресурсов (количество запросов, объем загружаемых файлов, размер данных) на уровне бизнес-логики.
- Проектируй так, чтобы каждый компонент системы получал ровно те полномочия, которые ему необходимы — не более.
- Используй готовые библиотеки и фреймворки для стандартных задач безопасности (аутентификация, хэширование, управление сессиями) вместо написания собственных реализаций.
- Если в user story есть действие, подумай: «А что будет, если кто-то сделает это сто тысяч раз? А если подставит чужой идентификатор?» Это обычная инженерная культура проверки граничных условий. Не надейся только на «happy path» — убедись в корректности всех веток выполнения.
- Конфигурация: используй безопасные значения по умолчанию
Ошибки конфигурации полностью попадают в зону ответственности разработчика. Здесь правило такое: приложение должно быть безопасным «из коробки», сразу после развертывания, а небезопасное поведение должно требовать явного включения.
- Не оставляй токены, дефолтные учетные записи и пароли в продакшене (вот серьезно — это до сих пор встречается повсеместно).
- Отключай подробные сообщения об ошибках и стектрейсы для внешних пользователей — оставь их только для разработки и тестирования.
- Удаляй из дистрибутива все, что не нужно в продакшене: тестовые страницы, документацию фреймворка, примеры кода.
- Задавай заголовки безопасности (Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security): большинство фреймворков позволяют сделать это в одном месте.
- Автоматизируй настройку окружения, чтобы конфигурация продакшена была воспроизводимой и проверяемой, а не формировалась за счет ручных правок на сервере.
- Зависимости: знай, что ты используешь
Уязвимые, устаревшие или атакованные компоненты — бич нашего времени, справиться с которым можно только через дисциплину управления зависимостями.
- Веди учет всех используемых библиотек и их версий, включая транзитивные зависимости.
- Настрой автоматическое сканирование на известные уязвимости — Dependabot, Snyk, OWASP Dependency-Check или аналоги.
- Обновляй зависимости регулярно. Не позволяй им устаревать до состояния, когда обновление превращается в болезненную миграцию, но при этом не спеши: дай новой версии время «отлежаться».
- Используй библиотеки из проверенных источников, выбирай пакеты с активным сопровождением.
- Удаляй неиспользуемые зависимости: каждый лишний пакет увеличивает поверхность возможных проблем.
- Не используй «latest» в качестве идентификатора версии компонента — фиксируй конкретные версии.
- Аутентификация: используй готовое, настраивай правильно
Сбоев идентификации и аутентификации можно избежать, если правильно использовать проверенные инструменты на адекватном уровне абстракции.
- Применяй готовые библиотеки и фреймворки для стандартных задач безопасности, НЕ пиши собственную реализацию проверки/сброса паролей и управления сессиями.
- Серьезно, не пиши: в этом ровно ноль смысла и сколько угодно потенциальных проблем.
- Включай многофакторную аутентификацию везде, где это возможно.
- При регистрации проверяй пароли пользователей на минимальную сложность и на наличие в списках скомпрометированных паролей.
- Генерируй идентификаторы сессий на стороне сервера с использованием криптографически стойкого генератора (а еще лучше использовать для этого средства, предоставляемые фреймворком).
- После выхода пользователя полностью инвалидируй его сессию.
- Не ограничивай количество неудачных попыток входа с помощью задержек или временной блокировки только по аккаунту — это DOS-условия. Также приостанавливай действия по IP и User Agent.
- Целостность: проверяй, что получаешь
Нарушение целостности ПО и данных говорит об излишнем доверии к тому, что приходит извне.
- Проверяй контрольные суммы и подписи при получении внешних компонентов, обновлений и пакетов.
- Не включай зависимости напрямую с публичных CDN без проверки целостности (SRI — Subresource Integrity).
- В CI/CD-пайплайне настрой контроль доступа и ревью кода, чтобы он не попадал в продакшен без проверки.
- Если приложение принимает сериализованные данные от клиента, не десериализуй их без валидации структуры и проверки подписи или используй простые форматы (JSON) вместо нативной сериализации. Не используй восстановление типов по сериализованным данным.
- Логирование: записывай то, что поможет разобраться
Недостатки логирования и мониторинга — это про наблюдаемость системы.
- Логируй все значимые события: вход/выход пользователей, неудачные попытки входа, изменение прав доступа, ошибки авторизации, критичные операции с данными.
- Следи за тем, чтобы в логи не попадала чувствительная информация — пароли, токены, персональные данные.
- Убедись, что логи записываются в формате, пригодном для автоматического анализа.
- Не пиши данные в логи, конкатенируя пользовательский ввод с шаблоном сообщения, — используй структурированное логирование, где данные и шаблон разделены.
- Настрой алертинг на аномалии (всплеск ошибок авторизации, необычно большое количество запросов и т. п.) — это полезно не только для безопасности, но и для мониторинга приложения в целом.
- Внешние запросы: ограничивай, куда ходит твой сервер
Подделка серверных запросов (SSRF) возникает, когда приложение без ограничений выполняет сетевые запросы к адресам, полученным от пользователя.
- Если приложение должно обращаться к внешним ресурсам по URL, IP или полученным извне доменам, ограничь множество допустимых адресов белым списком протоколов, хостов и портов.
- Не позволяй обращаться к внутренним адресам (localhost, 169.254.169.254, приватные подсети) или, если этого требует бизнес-логика, хотя бы к произвольным внутренним адресам.
- Не возвращай клиенту сырой ответ от внешнего сервиса, извлекай только нужные данные.
- Если функциональность позволяет, вообще не давай пользователю указывать произвольный URL —предлагай выбор из заранее определенного набора, которого требует бизнес-логика.
Главный принцип
Все эти десять правил (кроме разве что логирования) объединяет одна идея: уязвимость — это реализация избыточной функциональности, которая не требуется в рамках разрабатываемых фич. Каждый раз, когда код принимает данные без валидации, конструирует запросы конкатенацией строк, предоставляет доступ по умолчанию или обращается к произвольному адресу, он делает чуть больше, чем требуется. Именно это «чуть больше» потом и используют атакующие для построения дерева атак.
Минимизируй точки воздействия на приложение, ограничивай входы и выходы, следуй бизнес-логике, используй проверенные инструменты и компоненты зависимостей — и значительная часть проблем безопасности потеряет актуальность, потому что в коде просто не будет того, что можно эксплуатировать.
Чек-лист
Контроль доступа
- Доступ ко всем непубличным ресурсам запрещен по умолчанию
- Каждая операция проверяет принадлежность данных текущему пользователю
- Логика авторизации централизована и переиспользуется
- Правила доступа покрыты тестами
Защита данных
- Чувствительные данные определены и классифицированы по проекту
- Ненужные чувствительные данные не хранятся (удаляются/анонимизируются)
- Пароли хранятся только в виде хэша (bcrypt, Argon2 или аналог)
- Данные между клиентом и сервером передаются по TLS
- В репозитории и конфигурационных файлах нет секретов
- Для генерации токенов, ключей, IV и соли используется CSPRNG
Входные и выходные данные
- Исполняемый код и описатели объектов не формируются из сырых входных данных
- Входные данные валидируются на стороне сервера (допустимые символы, длина, формат)
- Выходные данные проходят контекстно зависимую санитизацию
- Валидация и санитизация применяются совместно, не заменяя друг друга
Архитектура
- Для каждой фичи явно определены допустимые действия и ограничения
- Ограничения валидируются на сервере, а не только на стороне клиента
- Установлены лимиты на потребление ресурсов на уровне бизнес-логики
- Каждый компонент получает минимально необходимые полномочия
- Проверены все ветки выполнения, не только «happy path»
Конфигурация
- Дефолтные учетные записи, пароли и токены не используются в продакшене
- Подробные сообщения об ошибках и стектрейсы отключены для внешних пользователей
- Из дистрибутива удалено все лишнее (тестовые страницы, примеры, документация фреймворка)
- Заголовки безопасности настроены (CSP, X-Content-Type-Options, HSTS)
- Настройка окружения автоматизирована и воспроизводима
Зависимости
- Ведется учет всех зависимостей, включая транзитивные
- Настроено автоматическое сканирование на известные уязвимости
- Версии зависимостей зафиксированы (без «latest»)
- Неиспользуемые зависимости удалены
- Зависимости получены из проверенных источников
Аутентификация
- Аутентификация реализована через готовые библиотеки
- Пароли проверяются на минимальную сложность и компрометацию
- Идентификаторы сессий генерируются серверной стороной через CSPRNG
- Сессия полностью инвалидируется после выхода
- Ограничение попыток входа учитывает не только аккаунт, но и IP + User Agent
Целостность
- Внешние компоненты и обновления проверяются по контрольным суммам/подписям
- Ресурсы с CDN подключены с проверкой целостности (SRI)
- В CI/CD настроен контроль доступа и обязательное ревью
- Нативная десериализация не используется для данных от клиента (или проверяются структура и подпись)
Логирование
- Логируются все значимые события (вход, выход, ошибки авторизации, критичные операции)
- Чувствительные данные не попадают в логи
- Используется структурированное логирование (данные отделены от шаблона)
- Настроен алертинг на аномалии
Внешние запросы
- Адреса для серверных запросов ограничены белым списком (протоколы, хосты, порты)
- Обращения к внутренним адресам запрещены или ограничены конкретным набором
- Клиенту не возвращается сырой ответ от внешнего сервиса
- Произвольные URL запрещены, вместо этого — выбор из определенного набора (если бизнес-логика позволяет)



