Как писать безопасный код, оставаясь разработчиком

  • #Безопасность_Приложений

О чем материал

Разбираемся, как повысить безопасность приложений через повседневные практики — без глубокого погружения в AppSec

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

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

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

OWASP Top 10 — общепризнанный хит-парад критичных рисков безопасности веб-приложений. Обычно его преподносят как каталог атак, с которого аппсекам стоит начинать работу над безопасностью проекта. Но каждый из десяти пунктов этого топа можно представить в виде простых правил кодинга, понятных любому разработчику. Давайте попробуем! 

Сейчас я публикую в своем telegram-канале цикл материалов про ИБ для разработчиков — там еще больше полезного

  1. Контроль доступа: не давай больше, чем нужно

Вместо изучения способов подмены идентификатора ресурса в URL, достаточно следовать простому принципу: каждая операция с данными должна проверять, что текущий пользователь имеет право именно на эту операцию именно с этим ресурсом. На практике это означает:

  • По умолчанию запрещай доступ ко всему, что не является публичным.
  • Реализуй логику авторизации один раз и переиспользуй, а не разбрасывай проверки по контроллерам.
  • Привязывай каждый набор данных к владельцу и проверяй принадлежность при любом обращении.
  • Покрывай правила доступа тестами — как обычную функциональность, без мыслей о защите от атак.
  • Авторизация — это бизнес-логика. Если она реализована полно и корректно, атакующему просто нечего будет эксплуатировать.
  1. Данные: защищай то, что не должно быть публичным

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

  • Не храни то, что не нужно хранить, — удаляй или анонимизируй данные сразу, как только они выполнили свою задачу.
  • Пароли храни только в виде хэша с использованием современного алгоритма (bcrypt, Argon2) — никогда в открытом виде и никогда через MD5/SHA-256.
  • Используй TLS везде, где передаются данные между клиентом и сервером.
  • Не складывай секреты (ключи API, пароли баз данных, токены) в исходный код или конфигурационные файлы, которые попадают в репозиторий.
  • Для создания токенов, ключей, векторов инициализации и соли используй криптографически стойкие генераторы случайных чисел — они есть в стандартных библиотеках любого языка.
  1. Входные данные: доверяй только тому, что сам определил

Разработчику не нужно знать десятки способов обхода WAF или отличия SQL-инъекции от инъекций команд ОС, но важно запомнить два правила:

  1. Никогда не конструируй на основе сырых входных данных то, что станет исполняемым кодом (SQL, HTML, команды ОС, LDAP-запросы) или описателем объекта (IP-адреса и доменные имена, файловые пути и URL, идентификаторы в хранилищах).
  2. Чтобы данные перестали быть сырыми, на входе используй валидацию (синтаксическая, семантическая) и/или типизацию, а на выходе — санитизацию (очистка, экранирование).

Как их применять:

  • Для вывода данных наружу используй контекстно зависимую санитизацию: смотри не только куда попадают данные, но и в каком грамматическом контексте они окажутся.
  • Помни, что санитизация не отменяет валидации/типизации и наоборот. Это разные механизмы, решающие разные задачи.
  • Определяй допустимые множества для всех входных данных (белый список допустимых символов или regex-шаблон, длину, формат) и валидируй ввод на стороне сервера. Суть в том, чтобы входные данные всегда оставались именно данными и никогда не становились кодом (даже когда попадают внутрь другого кода).
  1. Архитектура: думай о границах доверия на этапе проектирования

Небезопасный дизайн — это когда проблемы нельзя исправить на этапе реализации, потому что они изначально заложены в архитектуру. Как этого избежать:

  • При проектировании фичи явно определяй, кто может совершать определенные действия и с какими ограничениями.
  • Не полагайся на то, что пользовательский интерфейс не позволит выполнить недопустимое действие, — валидируй ограничения на сервере.
  • Устанавливай лимиты на потребление ресурсов (количество запросов, объем загружаемых файлов, размер данных) на уровне бизнес-логики.
  • Проектируй так, чтобы каждый компонент системы получал ровно те полномочия, которые ему необходимы — не более.
  • Используй готовые библиотеки и фреймворки для стандартных задач безопасности (аутентификация, хэширование, управление сессиями) вместо написания собственных реализаций.
  • Если в user story есть действие, подумай: «А что будет, если кто-то сделает это сто тысяч раз? А если подставит чужой идентификатор?» Это обычная инженерная культура проверки граничных условий. Не надейся только на «happy path» — убедись в корректности всех веток выполнения.
  1. Конфигурация: используй безопасные значения по умолчанию

Ошибки конфигурации полностью попадают в зону ответственности разработчика. Здесь правило такое: приложение должно быть безопасным «из коробки», сразу после развертывания, а небезопасное поведение должно требовать явного включения.

  • Не оставляй токены, дефолтные учетные записи и пароли в продакшене (вот серьезно — это до сих пор встречается повсеместно).
  • Отключай подробные сообщения об ошибках и стектрейсы для внешних пользователей — оставь их только для разработки и тестирования.
  • Удаляй из дистрибутива все, что не нужно в продакшене: тестовые страницы, документацию фреймворка, примеры кода.
  • Задавай заголовки безопасности (Content-Security-Policy, X-Content-Type-Options, Strict-Transport-Security): большинство фреймворков позволяют сделать это в одном месте.
  • Автоматизируй настройку окружения, чтобы конфигурация продакшена была воспроизводимой и проверяемой, а не формировалась за счет ручных правок на сервере.
  1. Зависимости: знай, что ты используешь

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

  • Веди учет всех используемых библиотек и их версий, включая транзитивные зависимости.
  • Настрой автоматическое сканирование на известные уязвимости — Dependabot, Snyk, OWASP Dependency-Check или аналоги.
  • Обновляй зависимости регулярно. Не позволяй им устаревать до состояния, когда обновление превращается в болезненную миграцию, но при этом не спеши: дай новой версии время «отлежаться».
  • Используй библиотеки из проверенных источников, выбирай пакеты с активным сопровождением.
  • Удаляй неиспользуемые зависимости: каждый лишний пакет увеличивает поверхность возможных проблем.
  • Не используй «latest» в качестве идентификатора версии компонента — фиксируй конкретные версии.
  1. Аутентификация: используй готовое, настраивай правильно

Сбоев идентификации и аутентификации можно избежать, если правильно использовать проверенные инструменты на адекватном уровне абстракции.

  • Применяй готовые библиотеки и фреймворки для стандартных задач безопасности, НЕ пиши собственную реализацию проверки/сброса паролей и управления сессиями.
  • Серьезно, не пиши: в этом ровно ноль смысла и сколько угодно потенциальных проблем.
  • Включай многофакторную аутентификацию везде, где это возможно.
  • При регистрации проверяй пароли пользователей на минимальную сложность и на наличие в списках скомпрометированных паролей.
  • Генерируй идентификаторы сессий на стороне сервера с использованием криптографически стойкого генератора (а еще лучше использовать для этого средства, предоставляемые фреймворком).
  • После выхода пользователя полностью инвалидируй его сессию.
  • Не ограничивай количество неудачных попыток входа с помощью задержек или временной блокировки только по аккаунту — это DOS-условия. Также приостанавливай действия по IP и User Agent.
  1. Целостность: проверяй, что получаешь

Нарушение целостности ПО и данных говорит об излишнем доверии к тому, что приходит извне.

  • Проверяй контрольные суммы и подписи при получении внешних компонентов, обновлений и пакетов.
  • Не включай зависимости напрямую с публичных CDN без проверки целостности (SRI — Subresource Integrity).
  • В CI/CD-пайплайне настрой контроль доступа и ревью кода, чтобы он не попадал в продакшен без проверки.
  • Если приложение принимает сериализованные данные от клиента, не десериализуй их без валидации структуры и проверки подписи или используй простые форматы (JSON) вместо нативной сериализации. Не используй восстановление типов по сериализованным данным.
  1. Логирование: записывай то, что поможет разобраться

Недостатки логирования и мониторинга — это про наблюдаемость системы.

  • Логируй все значимые события: вход/выход пользователей, неудачные попытки входа, изменение прав доступа, ошибки авторизации, критичные операции с данными.
  • Следи за тем, чтобы в логи не попадала чувствительная информация — пароли, токены, персональные данные.
  • Убедись, что логи записываются в формате, пригодном для автоматического анализа.
  • Не пиши данные в логи, конкатенируя пользовательский ввод с шаблоном сообщения, — используй структурированное логирование, где данные и шаблон разделены.
  • Настрой алертинг на аномалии (всплеск ошибок авторизации, необычно большое количество запросов и т. п.) — это полезно не только для безопасности, но и для мониторинга приложения в целом.
  1. Внешние запросы: ограничивай, куда ходит твой сервер

Подделка серверных запросов (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 запрещены, вместо этого — выбор из определенного набора (если бизнес-логика позволяет)

Мы дěлаем Positive Research → для ИБ-экспертов, бизнеса и всех, кто интересуется ✽ {кибербезопасностью}