Всем привет. Меня зовут Нияз Кашапов, я AppSec Lead в СберМаркете. Улучшаю процессы безопасной разработки уже более 5 лет. Начинал карьеру в финтехе, где занимался безопасностью кода, фич и бизнес-процессов в онлайн-банкинге. А сейчас продолжаю начатое в одном из самых быстрорастущих игроков на рынке e-com.
Думаю, у многих в практике встречалась уязвимость, которую просто так не пофиксить — ведь она заложена глубоко внутри разрабатываемого решения, обвешана кучей зависимостей и требует полного ребилда самого решения. Чаще всего такие уязвимости остаются в проде навсегда и удерживаются от «падения» множеством костылей. Возникает резонный вопрос: «Как они возникли?» Чаще всего ответ — «Так исторически сложилось», а истоки проблемы давно забыты. Боролься с таким лучше превентивно, а как это сделать — попробую рассказать в этой статье.
Поговорим о том, как избегать ситуаций, когда уязвимость заложена на уровне архитектуры или бизнес-процесса и её исправление может стоить множество человеко-часов. Разберемся, когда фича становится багом и как прорабатывать архитектуру сервисов, не создавая дыры безопасности.
Боль и страдания фикса архитектурных уязвимостей
На старте своей карьеры я не один раз сталкивался с такими уязвимостями. Их сложно отнести к классическим уязвимостям кода (инъекции, валидации, загрузка файлов) — они были непосредственно в бизнес-логике приложения.
Вот несколько примеров таких проблем. Все совпадения с реальностью случайны. Все примеры упрощены для показательности.
Кейс 1. Уязвимость с указанием суммы платежа на фронтенде
Это классика! Из самого названия кейса понятно, где кроется проблема.
В одной довольно распространенной платежной системе, которую используют в интернет-магазинах, обнаружили уязвимость, позволяющую злоумышленнику изменить сумму покупки при оплате товара или услуги. На последнем этапе покупки — в момент отправки запроса на оплату — мошеннику требовалось изменить параметр суммы платежа paymentSum в меньшую сторону. Конечная сумма никак не валидировалась магазином. Из-за этого возникали случаи, когда товар за несколько тысяч рублей можно было купить за 11 рублей.
Как этого можно избежать? Продумать процесс оплаты таким образом, чтобы конечная сумма покупки всегда отправлялась только непосредственно магазином через back-2-back или же через фронт покупателя, но с проверкой целостности отправленных данных.
Кейс 2. Отсутствие возможности кикнуть все активные сессии
Думаю, с таким кейсом сталкивались многие. В системе аутентификации не предусматривалась возможность досрочного завершения сессии по запросу пользователя или техподдержки компании. То есть кто-то мог методами социнженерии выпросить одноразовый код доступа и войти с нового устройства. СМС с уведомлением, конечно же, приходило, но клиент ничего не мог с ним поделать. А техподдержка могла удалить сессию только непосредственно в базе данных.
Похожий кейс — уязвимость вечноживущих Refresh Tokenов. Разработчики ожидают, что пользователь будет нажимать на кнопку Log out и тем самым деактивирует активный токен. Однако пользователи просто закрывали вкладку — и токен оставался в хранилище браузера. В течение долгого времени любой, кто имел доступ к браузеру, мог забрать токен и использовать его для выпуска сессии по новому Access token.
Как этого можно избежать? Не торопиться с выкаткой незавершенного сервиса аутентификации в прод, учесть требования по управлению сессиями при разработке сервиса.
Кейс 3. Возврат денег за доставленный заказ
Необычный кейс, который может появиться из-за микросервисной архитектуры.
Обычно, покупатель проходит путь «корзина → холдирование денег → ожидание курьера → получение заказа → списание денег». Схема работает прекрасно, но только до появление доставки собственными силами магазина. Путь ломается, когда шаг «Ожидания курьера» пропускается, так как отследить его местоположение нет возможности. В таком случае подтверждение заказа происходит только когда курьер возвращается в магазин. В это время покупатель, уже получивший заказ, может отправить запрос на его отмену. Такое возможно из-за рассогласованности сервисов.
Как этого можно избежать? Учесть кейс доставки внешними силами и давать возможность отменить заказ только до конечной сборки заказа.
Кейс 4. Аутентификационные токены в GET-параметрах. POST-как-GET и балансировщики
Еще один пример того, как легаси может стать головной болью для всех.
Допустим, в приложении так исторически сложилось, что аутентификационные токены передаются в параметрах GET-запроса. Вроде ничего страшного, но с приходом балансировщиков, кешей и большого количества функционала контролировать утечку аутентификационных токенов стало непосильной задачей.
Внезапно довольно большой проблемой могут стать… файлы. Они открываются только аутентифицированным пользователям — и в адресной строке, конечно же, находится аутентификационный токен. Не знающие об этом пользователи копируют ссылки и отправляют их друзьям, коллегам и различным сервисам, которые эти ссылки хранят у себя (привет, сокращатели ссылок!). Конечно же, случаев утечки данных в таком случае может стать в разы больше и придется переделывать показ файлов на схему с одноразовой ссылкой.
Как этого можно избежать? При первичном написании сервисов не использовать аутентификацию по GET-параметру. А если серьезно, учесть особенность системы и сразу заложить работу с одноразовыми ссылками на объекты через прокси или S3-хранилища.
Учимся на ошибках
Почти все эти кейсы объединяет то, что при разработке сервиса команды не учитывали требования безопасности самой фичи и не рассматривали нетривиальные кейсы использования своих систем.
Все вышеупомянутые кейсы показывают, что в сложных системах легко допустить ошибку, особенно в погоне за time-to-market. Чаще всего аналитики, архитекторы и разработчики не думают о безопасности как об основе или забывают, что в жестоком мире бывают фродеры и хакеры. Так рождаются уязвимости в самых глубинах разрабатываемых систем.
Не можешь контролировать — возглавь
Как безопасник скажу: неприятно узнавать об изменениях и подобных косяках в последнюю очередь. За годы работы стало понятно: вместо того, чтобы рисовать модели угроз на основе уже готовых систем и копировать схемы из источников, необходимо получать схемы сервисов, которые существуют ещё только на уровне идеи.
Если помочь разработчикам на ранних этапах проработки задач и сформулировать требования безопасности, можно разом закрыть большой скоуп потенциальных проблем.
Вот лишь небольшой список уязвимостей, которые можно предотвратить на этапе проработки требований и архитектуры:
SQL-инъекции — уязвимости, возникающие при некорректной обработке входных данных, которые позволяет злоумышленнику выполнить произвольный SQL-запрос к базе данных. Именно на уровне архитектуры можно определить источники данных, определить доверенные они или нет, может быть добавить промежуточные сервисы-адаптеры, контролирующие входящие данные.
Business Logic Errors — уязвимости логики приложения позволяющие пропускать шаги бизнес-процесса или вызывать те или иные методы API в обход условий выполнения. Контроль процессов и условий выполнения должен быть согласован для исключения фрод-схем или некорректного вызова методов.
Broken Access Control — атаки, при которых злоумышленник может получить доступ к чужим данным при обращении по идентификатору или подмене параметров отображения. На уровне требований можно корректно настроить доступы и использовать идентификаторы объектов с учетом возможных переборов.
DoS/DDoS-атаки — атаки, направленные на отказ в обслуживании или распределённый отказ в обслуживании системы. Анализ архитектуры позволяет найти узкие места или отсутствие механизмов защиты от целенаправленных атак.
Фрод — мошенничества, цель которых — получение персональной выгоды с помощью абьюза механик.
Говорю по своему опыту — часто в компаниях будущие сервисы прорабатывают следующим образом:
Сперва требования составляют бизнес-аналитики или продакт-менеджеры.
Дальше на их основе системные аналитики проводят свою работу и формируют технические требования и условия.
После этого команда разработки начинает реализовывать сервис.
Вопрос: где тут забота о безопасности?
Кроме проблем безопасности, в больших командах, разрабатывающих микросервисы, могут возникать проблемы несогласованности — команды иногда ленятся сходить к «соседям» и согласовать изменения.
Для исключения случаев, когда сервисы противоречат друг другу или дублируют функционал, нужно иметь централизованное управление изменениями, чтобы понимать полную картину и на ранних этапах выявлять зависимости и сложности интеграции.
Мы в СберМаркете начинали с очных встреч по архитектуре, где разбирали инициативы по доработке сервисов. Приносили схемы, требования к сервисам, бизнес-цели. Каждую неделю мы разбирали по одной-две инициативы. Однако это было удобно пока количество сервисов не перевалило за 100+. Затем вовлеченных лиц стало слишком много и не всегда получалось собрать кворум.
Так и появилась идея внедрить асинхронный процесс system design review.
System design review — это процесс анализа и оценки архитектуры программного продукта, в ходе которого выявляют потенциальные недостатки, в том числе в безопасности самого сервиса. Этот этап критически важен для надежности и безопасности программного решения, поскольку позволяет предотвратить многие проблемы еще на этапе проектирования. Анализ проводят архитекторы, тимлиды изменяемых систем и представители команд безопасности и эксплуатации.
В СберМаркете system design review — это отдельный процесс, который пропускает через себя все новые сервисы и крупные изменения систем. На этапе создания нового сервиса или возникновения идей о доработке оформляется документ в виде архрешения — architecture decision record (коротко ADR). В нем отображается и максимально подробно описывается изменение ландшафта — в виде описания изменяемого бизнес-процесса, списка use-case и добавляемых или изменяемых контрактов. Для отслеживания сами изменения заводятся как задачи и именуются RFC (request for comments).
Сама архитектура описывается в виде C4-диаграммы, use-case аналогично отображены в виде потоков данных или событий, контракты описываются в виде proto или swagger файлов. Это помогает генерировать сервисы и клиенты с помощью кодогенерации из контрактов.
Структура самого документа стандартизирована:
Документ описывается в гите, изменения предлагаются для обсуждения в виде Merge Request. Это очень удобная история: можно накидать CODEOWNERS и разделить ответственность. Апруверы выделяются по доменам/системам/функционалу. Аналогично, выделены апруверы со стороны ИБ и платформы.
На этапе сбора комментариев архитекторы, техлиды и безопасники могут накидывать вопросы и предложения.
Представители безопасности подсвечивают те самые кейсы, которые могут вылиться в уязвимости. Кроме того, обязательно выставляют требования безопасности для новых ручек и предложения по корректной реализации авторизации к ресурсам. На этом этапе можно начать формировать полноценные модели угроз для новых сервисов и конкретных функциональностей.
Пример комментария с указанием требования к валидации загрузки файлов (в описании use-case):
Если собирается необходимое количество апрувов, доработка считается согласованной и идет в работу — начинается разработка кода.
Как только она завершается, изменения снова приходят на аудит. Тут уже проверяется безопасность кода и соответствие предлагаемой архитектуре.
Ключевые аспекты безопасности, на которые мы обращаем внимание при проведении system design review:
Требования безопасности. Убеждаемся, что они чётко определены и соответствуют текущим стандартам и нормам — проверить наличие механизмов аутентификации, авторизации, шифрования данных и других мер безопасности.
Использование лучших практик. Проверяем, что предлагаемая архитектура применяет лучшие практики безопасности — HTTPS, шифрование данных, многофакторную аутентификацию и другие.
Оценка архитектуры. Анализируем архитектуру системы на предмет возможности возникновения уязвимостей. Например, проверяем, возможна ли обработка недоверенных данных, приводящая х к SQL-инъекциям, XSS-атакам, CSRF-атакам и другим распространенным угрозам. Кроме того, можно оценить архитектуру с точки зрения устойчивости к DoS и DDoS атакам (наличие механизмов защиты и отсутствие узких мест).
Анализ рисков: Проводим анализ рисков, связанных с возможными уязвимостями системы в рамках бизнес-процесса, и разрабатываем план, как их устранить или минимизировать.
Создание безопасной архитектуры веб-приложений требует комплексного подхода, и в ходе ревью нужно акцентировать внимание на лучших практиках безопасности:
Разделение привилегий. Принцип наименьших привилегий нужно применять на всех уровнях приложения, включая доступ к базе данных, управление сессиями и системные ресурсы. Это означает, что каждый компонент приложения должен иметь только те права, которые необходимы для выполнения его функций.
Изоляция компонентов. Разработка приложения с четко разграниченными компонентами помогает минимизировать ущерб при возможных атаках и облегчает процесс обновления и исправления уязвимостей. Сервисы делятся по бизнес процессам, и если сервисы не должны взаимодействовать каким либо образом, то и соответственно доступности между ними быть не должно. Реализуется через service mesh или сетевые политики.
Аутентификация и авторизация. Строгая политика аутентификации и управления доступом, основанная на проверенных стандартах и фреймворках (OAuth, OpenID Connect и JWT), обеспечивает надежную идентификацию пользователей и контроль доступа к ресурсам.
Управление доступом на основе атрибутов (ABAC). Позволяет определять права доступа, используя детальные политики, которые основаны на атрибутах пользователей, объектов, действий и окружения.
Шифрование. При передаче данных в недоверенных сетях и инфраструктуре важно пользоваться шифрованием. Исходя из кейса, нужно выбирать симметричные или асимметричные алгоритмы шифрования.
Работа с файлами. При приеме файлов от клиентов стоит очень осторожно относится к загружаемым данным — проверять их не только по расширению, но и по mime-type и magic bytes.
Лимиты для ресурсов. Любые ресурсы должны иметь свои ограничения. Это касается входящего и исходящего трафика, ресурсов, которые потребляются сервисами, ограничения по использованию памяти и т.д.
Следуя этим рекомендациям, мы создаем безопасную архитектуру веб-приложений, минимизируя риски несанкционированного доступа к данным и обеспечивая надежную защиту системы.
Нет предела совершенству. Что еще можно сделать?
На самом деле процесс работает отлично. С точки зрения команды ИБ на основе ревью архитектуры хотелось бы сразу генерировать типовые и частные модели угроз — с последующей конвертацией в задачи для команд разработки. Это позволяет следить за угрозами и рисками и превентивно закрывать любые проблемы.
Уже сейчас мы реализовали генерацию схем актуальной архитектуры на основе описания и выгрузки из платформы — об этом писал Кирилл Ветчинкин в своей статье (кстати, она выиграла Технотекст 2023 в номинации «Подготовка технической документации» уровня Senior). Есть планы по заимствованию этой схемы для последующей генерации моделей угроз на основе настроек гейтвеев, доступа к данным, наличию/отсуствию доступных извне ручек и ручной обработке usecase со стороны Application Security или риск менеджера.
Подводя итоги
Хотелось бы вернуться к примерам из начала статьи и напомнить себе и читателю, для чего вообще я решил написать этот текст. Продукты стали сложными, и требуют хорошей проработки в глубину. Даже если это MVP или экспериментальный продукт, надо отнестись к проработке архитектуры максимально серьезно. Часто бывает, что большие системы вырастают из недоработанных MVP и наследуют ворох проблем, в том числе и с безопасностью.
Мы у себя внедрили большой процесс, помогающий ловить 80% критических проблем с безопасностью, исправление которых стоит минимальных усилий (нет кода → нет фиксов → не тратим время разработчиков). Считаю, что практика успешная, и ее стоит внедрять всем компаниям — особенно на ранних этапах.
Tech-команда Купера (ex СберМаркет) ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.