Любое программное обеспечение неизбежно содержит уязвимости, причем на любом этапе своего жизненного цикла. Избавиться от изъянов невозможно, однако всегда полезно применять испытанные методы и приемы, позволяющие сократить число потенциальных брешей. Практический опыт создания безопасных программных продуктов, представленный с точки зрения активно развивающегося системного интегратора, пригодится и корпоративному, и индивидуальному разработчику.
Введение
Продукт разработки программного обеспечения (далее — ПО) для внутренних нужд или для внешнего заказчика гарантированно уязвим в определенной мере. Причины этого очевидны. Существуют проблемы языка разработки (библиотек, среды выполнения runtime): например, в приложениях, созданных на языке Java, регулярно находят уязвимости. Уязвимы сами платформы функционирования и разработки. Не идеальны внешние подключаемые модули и дизайн программного обеспечения. Зачастую плохо организован сам процесс разработки.
Отдельного упоминания заслуживает процесс получения обновлений приложений, его недостаточная продуманность и организованность.
Наконец, причиной появления уязвимостей могут служить непосредственно ошибки и недоработки программистов: плохая преемственность разработчиков и как следствие наслоение кода, появление лишних сущностей, отсутствие проверки ввода и вывода, документирования и комментирования кода, отсутствие унификации и так далее.
Сказываются и давление со стороны бизнеса (диктуются сроки и программные доработки — feature requests), и отсутствие понимающей позиции у коллег-безопасников, сетевиков, инфраструктурщиков (один сервер под множество баз данных разного уровня доверия) — эти факторы приводят к увеличению рисков информационной безопасности ПО. Например, нередка ситуация: программная доработка требует организации нового сетевого общения между серверными компонентами, но согласование нового сервиса требует такого количества времени, которое в соблюдение даты закрытия запроса на доработку (feature request) не вписывается; разработчик ускоренно «ковыряет дырку» в знакомом месте, в обход политики ИБ, возможно, даже с временным согласованием безопасника при условии, что потом все будет сделано как следует, но почти наверняка это «потом» не наступает, и в архитектуре остается уязвимость.
Как показывает практика, многих проблем можно избежать, если заранее учитывать эти и другие подобные факторы.
Стратегия уменьшения рисков
Абсолютно безопасное приложение создать невозможно, как и панацею от всех болезней, но качественная организация разработки вполне способна минимизировать риски. Можно применить так называемую «модель швейцарского сыра»: сыр многослоен, и в каждом слое есть дырки; слои сыра — это слои защиты (на уровне организации процесса и регламентов, на уровне архитектуры, на уровне кода и так далее), а дырки — это ошибки реализации. Если смотреть на сыр снаружи, то сквозных отверстий не видно (поскольку дырки в разных слоях не совпадают), а значит, система стабильна и безопасна. Соответственно, несмотря на наличие в каждом отдельном слое ошибок или недоработок, системе в целом удается избежать глобальной аварии.
Для внутренних приложений риски несанкционированного проникновения можно снизить за счет инфраструктурных решений. Для внешнего продукта и этот механизм является неконтролируемым, так что возможно лишь дать рекомендации к архитектуре установки и к организации ее информационной безопасности.
Для снижения рисков в конечном продукте рекомендуется проработать и усилить следующие аспекты разработки.
- В части организации процесса разработки:
- использовать проработанные фреймворки;
- осуществлять необходимое и достаточное комментирование кода, ведение документации, использование внутреннего вики-ресурса;
- проводить регулярную ревизию кода. Конечно, этот процесс менее приоритетен, чем программные доработки, но выделять на него часть продуктивного времени разработчиков необходимо.
- В части организации процесса тестирования: обеспечить регулярность этого этапа и не игнорировать его, несмотря на желаемые сроки выхода продукта. Лучше выпустить продукт позже, чем некачественно протестированным. Кроме того, полезно использовать разные типы тестирования:
- динамическое и статическое тестирование на уязвимости, для которого существует целый класс решений — средства анализа кода;
- регрессионное тестирование продукта;
- фаззинг-тестирование (или fuzz-тестирование, fuzzing) — «техника тестирования программного обеспечения, автоматическая или полуавтоматическая, заключающаяся в передаче приложению на вход массива неправильно сформированных или случайных данных» (ист. ru.wikipedia.org) — ресурсоемкая процедура с менее высоким коэффициентом полезного действия, но при возможности полноценной реализации дающая свои результаты. ГОСТ Р 56939-2016 «Защита информации. Разработка безопасного программного обеспечения. Общие требования» рекомендует ее использование;
- периодическое проведение процедуры тестирования на проникновение (pentest).
- В вопросе дизайна и архитектуры ПО:
- использовать аутентификацию, в идеальном случае — на сертификатах, даже во внутреннем взаимодействии и даже для доверенного администратора или разработчика;
- отказаться от использования слабых протоколов передачи данных (ftp, http, smb);
- не класть «все яйца в одну корзину»: существует много решений виртуализации или контейнеризации — менее ресурсоемкий вариант, который также дает достаточный уровень изоляции сервисов друг от друга;
- изолировать сетевую среду, в первую очередь — отделять frontend-компоненты от критически важных частей ПО и баз данных;
- ограничить ввод и вывод данных: объем загрузки и выгрузки, количество и формат записей и так далее;
- отказаться от использования полных привилегий (root) на любых этапах взаимодействия, включая административные;
- использовать безопасные криптографические алгоритмы.
- В вопросе выбора платформы и компилятора:
- по возможности использовать платформы со встроенными проверками безопасности, технологии ASLR (рандомизация схемы адресного пространства), CRED (C range error detector) и др., а также компиляторы со встроенными контролями, например, GCC (GNU Compiler Collection), применять MSVC (Microsoft Visual C++);
- выполнять обновления платформы, компилятора, языка. Хотя эта ресурсоемкая операция не дает прямых выгод для продукта, уровень информационной безопасности и разработки заметно повышается.
- В вопросе анализа рисков:
- периодически моделировать угрозы с трансляцией результатов в отдел разработки;
- формировать у разработчиков продуманный подход к продукту через регулярное обучение.
Вот что рекомендует эксперт компании «Авито» Евгений Харченко:
«Фактически, нужно использовать подход «нельзя верить никому»:
- нельзя верить тому, что тебе передают по сети;
- нельзя верить тому, что тебе вводит пользователь, нельзя верить тому, что это действительно пользователь;
- нельзя верить сторонним компонентам и агентам, которые используешь;
- нельзя верить себе, и так далее.
Понятно, что в определенной степени этим компонентам верить приходится, в этом суть баланса безопасности и бизнес-задач. Но нужно быть готовым к ошибкам и подстилать соломку по мере возможности.
Обеспечивать безопасность внешнего трафика: например, если ожидаешь, что пользователь напишет тебе число, то что бы он ни прислал — приведи это к числу. Тогда будешь уверен, что дальше работаешь с числом, и в самом критическом случае избежишь shell-инъекции.
Не использовать сторонние компоненты, пока можешь их не использовать. Отправлять пользовательский ввод не в формате «взял-отдал», а в формате «взял-обработал-отдал». Следить за бюллетенями безопасности (Security Notes) к компонентам, устанавливать обновления, применять виртуал-патчинг.
Ограничивать физические доступы компонентов к внутренней инфраструктуре: например, если используешь сервис аутентификации, то не давать ему доступа к базе данных биллинга, и тогда в случае получения злоумышленниками контроля над ним диапазон потенциального риска будет ограничен.
Доводить это до паранойи не надо, но выполнять необходимые разграничения рекомендуется: использовать контейнеризацию или виртуализацию, ограничивать сетевые взаимодействия, выделять базы в защищенный контур, разделять их. Не допускать хождения всех сервисов с одними реквизитами в соседние таблички одной базы, как минимум разделять базы, возможно, использовать Docker или другие средства изоляции или виртуализации. Аккуратно организовывать сетевую видимость, не допускать лишних сервисов и так далее.
Полезно иметь базовую модель угроз, желательно — с типовыми примерами, и сделать ее доступной непосредственно команде разработки и даже обязательной к регулярному ознакомлению.
И, конечно, необходимо работать над внутренней культурой общения между подразделениями информационной безопасности и разработки. Каждый их представитель должен понимать, что первостепенная задача — это эффективное взаимодействие, а не попытка перекладывания ответственности с одних плеч на другие, и их общая цель — это качественный и своевременный продукт».
Стандарты и рекомендации
В вопросах организации и процессов безопасной разработки существует много стандартов и рекомендаций. 1 июня 2017 года введен в действие для добровольного применения ГОСТ Р 56939-2016 «Защита информации. Разработка безопасного программного обеспечения. Общие требования». Стандарт описывает комплекс мер, реализуемый в процессах функционирования и сопровождения ПО, формирования и поддержания среды обеспечения оперативного устранения выявленных пользователями ошибок ПО, направленный на снижение рисков появления уязвимостей. ГОСТ содержит перечень действий, которые рекомендуется осуществить на различных этапах жизненного цикла ПО, и является российским аналогом концепции SDL (Security Development Lifecycle).
В вопросе конкретных рекомендаций по разработке существует достаточно литературы с конкретными примерами — скажем, «Защищенный код» Майкла Ховарда и Дэвида Лебланка (Writing Secure Code), — а также отдельных узконаправленных статей, например, на сайте Habr (https://habr.com/ru/post/56291/, https://habr.com/ru/post/70330/). Кроме того, разработчикам полезно регулярно знакомиться с данными исследовательских организаций, в частности:
- Mitre, «CWE/SANS Top 25 Most Dangerous Software Errors» (https://cwe.mitre.org/top25/index.html),
- OWASP, «OWASP Top 10» (https://www.owasp.org/images/7/72/OWASP_Top_10-2017_(en).pdf.pdf),
- OWASP CheatSheetSeries (https://www.owasp.org/index.php/OWASP_Secure_Coding_Practices_-_Quick_Reference_Guide, https://github.com/OWASP/CheatSheetSeries),
- RedHat, «Best Practices Guide for Defensive Coding» (https://developers.redhat.com/topics/secure-coding/), и др.
Однако никакие решения и процедуры не могут дать гарантии безопасности приложений, поэтому необходимо иметь проработанный план реагирования на инциденты ИБ, а также рассматривать целесообразность использования наложенных средств защиты приложений, в том числе:
- защиты web-приложений WAF (Web Application Firewall),
- защиты DNS,
- защиты БД DBF (Database Firewall),
- разграничения и фильтрации сетевых взаимодействий уровня NGFW, UTM, IPS-решений,
- антивирусной защиты.
Кроме названных средств, в части статического и динамического тестирования выходного продукта существует упоминавшийся выше отдельный класс решений, направленных на поиск уязвимостей: анализаторы кода.
Средства анализа кода
Решения по анализу кода осуществляют поиск известных уязвимостей в разработанном коде ПО. Проверка выполняется как для исходного кода — статический анализ, или SAST (Static Application Security Testing), — так и для исполняемого — динамический анализ, или DAST (Dynamic Application Security Testing). Динамический анализ фактически является первым этапом тестирования на проникновение (penetration test) для приложения.
Различные реализации анализаторов поддерживают свои наборы языков программирования, возможности по рекомендациям и исправлениям найденных ошибок программирования, базы уязвимостей, варианты использования в облаке или on-premise.
Подробный обзор продуктов класса анализаторов кода можно посмотреть в статье Екатерины Бойцовой: https://www.anti-malware.ru/reviews/Code_analyzers_market_overview_Russia_and_world
Выводы
Одну из основных целей организации информационной безопасности можно сформулировать упрощенно: сделать так, чтобы злоумышленнику было максимально сложно взломать систему и получить доступ к информации. Действия, описанные в этой статье, направлены на создание эшелонов защиты продукта на разных уровнях производства и функционирования, и в первую очередь — повышения уровня осведомленности команды разработки в вопросах ИБ как наиболее эффективного средства защиты информации.