Привет, Хабр! Я — Виталий Киреев, руковожу разработкой в хостинг-провайдере SpaceWeb. Сегодня поделюсь с вами базовыми практиками в области безопасности кода для веб-приложений и расскажу о пяти подходах, которые мы применяем в компании для повышения общей безопасности нашей экосистемы и продуктов. Эта статья будет интересна, прежде всего, для начинающих разработчиков и веб-мастеров.

Навигация по тексту:

Почему важна безопасность кода?

Безопасность кода — это база, то есть фундаментальная составляющая устойчивости цифровых продуктов. Общаясь с DevOps инженерами, архитекторами и разработчиками, можно увидеть, что у всех в целом есть понимание того, что уровни защиты L1, L2, L3, L4, L7 очень хорошо стандартизированы на уровне каналов, сетей, оборудования и т.д. Но действительно нет хороших и грамотных стандартов, которые бы позволили унифицировать подход к безопасности именного самого кода. Это часто приводит к появлению многочисленных однотипных уязвимостей, влекущих за собой разные последствия, особенно для веб-приложений, которые обычно являются точками взаимодействия внешних пользователей с внутренними сервисами компании через API.

Основная проблема заключается в разнице уровней знаний и подходов к написанию безопасного кода. Разработчики, работающие на разных позициях, могут по-разному интерпретировать и применять принципы безопасности. Более того, существующая практика использования множества сторонних библиотек и модулей добавляет сложностей. Эти элементы, конечно, упрощают разработку, но в то же время становятся потенциальным источником уязвимостей, если их безопасность не проверяется должным образом.

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

В нашей компании мы разработали практики, которые позволяют выявлять и классифицировать типовые уязвимости, а также минимизировать их повторное появление. Далее расскажу подробнее о них.

Сторонние библиотеки

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

Для минимизации этих рисков мы внедрили разный комплекс подходов. Во-первых, мы регулярно проводим анализ библиотек с помощью инструментов класса Software Composition Analysis — они позволяют выявить слабые места и классифицировать их по уровню критичности. Проекты сканируются перед каждым релизом, а критические уязвимости быстро фиксируются в бэклоге задач.

Во-вторых, централизация обновлений. Если библиотека используется в нескольких проектах, ее обновление должно быть синхронизировано, чтобы избежать расхождений в безопасности. Для этого мы, например, поддерживаем собственные локальные репозитории со стабильными дистрибутивами и используем локальные репозитории в процессе разработки. Они включают не только библиотеки, но и образы контейнеров для разработки и тестирования.

В третьих, кастомизация пакетов. Мы иногда делаем fork ключевых библиотек, чтобы адаптировать их под специфические задачи. Такой подход дает независимость, однако требует внимательного управления, чтобы не упустить важные обновления безопасности из основной ветки. Желательно регулярно обновлять fork оригинальным пакетом, а если это трудозатратно из-за сильных расхождений, то мы используем git-cherry-pick для интеграции критических изменений. Если и это невозможно, то имплементим изменения самостоятельно. 

Ну и, конечно, не забываем уделять особое внимание совместимости сред. К примеру, для устранения несовместимости между архитектурами x86_64 и AArch64 наши DevOps специалисты создают контейнеры, полностью соответствующие требованиям разработки и продакшена — что особенно важно, когда разработка ведется на разных устройствах, а итоговая сборка должна быть целостной и безопасной.

Паттерны в программировании

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

Например, если пользователь заполняет форму, валидатор на клиентской стороне должен гарантировать, что форма не будет отправлена, пока все поля не будут корректно заполнены. Этот процесс должен быть продублирован на backend, чтобы исключить подделку запросов (server-side-request-forgery) или попытки отправки некорректных данных через API-запросы. Валидаторы есть для большинства языков разработки, и это стандарт: будь то встроенные возможности HTML5, библиотеки для Python или валидаторы Symfony для PHP.

Однако, часто встречается путаница между валидаторами и санитайзерами. Санитайзеры, в отличие от первых, изменяют входящие данные для улучшения пользовательского опыта и повышения безопасности. Например, они могут обрезать лишние пробелы, экранировать HTML-теги или заменять специальные символы, чтобы предотвратить инъекционные атаки. 

Еще один важный механизм безопасности — привязка объектов к владельцу (Object Ownership). Особенно полезен, когда объект, к примеру сервер, доступен только определенному пользователю. Для защиты создается дополнительное свойство, связывающее объект с владельцем. Права доступа проверяются при каждом запросе, чтобы убедиться, что операции с объектом могут выполнять только разрешенные пользователи. Реализация может включать создание объектов только при соблюдении условий безопасности. 

class ServerApi

{

private string $customerId;

public function _construct( )

{

// сохраняем авторизованного пользователя

$this->customerId = $_SESSION [' customerId'];

}

// метод получения информации о сервере по его ID

public function info(int $serverId): ServerInfo

return $this->getServer ($serverId)->getInfo();

}

// фабричный метод для создания объекта

protected function getServer(int $serverId): Server

{

// в случае, если сервер с этим ID не принадлежит этому пользователю, будет ошибка

return ServerFactory:: createByIdAndCustomerId ($serverId, $this->customerId);

}

Для более сложных сценариев полезен Manager Design Pattern (на основе паттерна Mediator). Он позволяет вынести бизнес-логику, включая проверки на безопасность, в отдельный компонент — менеджер. Например, вместо того чтобы напрямую управлять сервером, запрос передается через менеджер, который проверяет права пользователя, баланс на счету и выполнение других условий. Если все проверки пройдены, менеджер передает управление объекту. Такой подход сохраняет чистоту архитектуры, снижает связность компонентов и упрощает сопровождение кода.

class ServerManager

{

// метод для включения сервера

public function turnOn (Server $server, Customer $customer): void

{

// проверим условия для запуска сервера

if ($server->getCustomerId() !== $customer->getId() ) {

// пользователь не владеет сервером

throw new AccessDeniedException();

}

// проверим положительный баланс

if ( $customer->getBalance () <= 0 ) {

// у пользователя недостаточно средств

throw new LowBalanceException();

}

// выполним логику метода

$server->boot();

}

Защита кода от утечек

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

Один из методов, который мы активно используем, — STUB-пакеты. Это заглушки, имитирующие поведение реальных модулей. В разработке играют роль полноценных компонентов, обеспечивая тестируемость и совместимость на уровне API, но без предоставления доступа к исходному коду других команд. Такой подход особенно полезен при работе по модели разработки через тестирование (TDD), когда спецификации и контракты формируют основу функциональности. STUB-пакеты обеспечивают работоспособность тестов, не раскрывая деталей реализации.

Для более сложных сценариев мы используем заглушки или MockAPI — сервисы, генерирующие фиктивные ответы на основе документации. Они позволяют изолировать разработку интерфейсов от реализации серверной логики. Например, во frontend-разработке мы создали библиотеку заглушек для локальной симуляции запросов, что устраняет зависимость от backend на начальных этапах.

А также для защиты frontend применяется обфускация. Этот процесс преобразует код в такой вид, который остается функциональным, но становится трудным для анализа. Например, плагины вроде Terser или UglifyJS позволяют не только скрыть исходный код, но и минимизировать его размер для продакшена. Но при этом обфускация не рекомендуется для backend, так как может замедлять выполнение и создавать сложности при интеграции. 

Данные клиента

Безопасность данных клиента — это область, где ответственность лежит не только на конечном пользователе, но и на разработчиках приложения. Для этого мы применяем комплексные методы, начиная с защиты интерфейса взаимодействия пользователя с системой и заканчивая контролем за перемещением данных внутри серверной архитектуры.

Content Security Policy (CSP) — современный стандарт, позволяющий ограничить загрузку внешних скриптов, стилей и других ресурсов. Он контролирует то, что загружается на страницу, и снижает риск возможных XSS-уязвимостей. Однако эта защита не универсальна. Например, GET-запросы, которые браузер выполняет напрямую, не подпадают под эти ограничения. 

Немаловажным будет грамотно настроить обработку cookie. К примеру, мы используем настройку same-site cookie с целью ограничения возможности их передачи на сторонние домены. Настройка Strict исключает любые междоменные запросы, тогда как настройка Lax допускает некоторую гибкость, особенно в сценариях с единой авторизацией. Дополнительно используем флаг HttpOnly, предотвращающий доступ к cookie из клиентского кода, и secure, требующий использования HTTPS. 

Логирование данных клиента на backend также необходимо защитить. Даже если клиентские данные минимально подвержены утечкам, необходимо учитывать, что журналы событий могут стать объектом интереса злоумышленников. Здесь на помощь приходят решения для маскирования чувствительных данных, таких как пароли, токены или персональная информация. В нашей практике мы используем специализированные библиотеки для маскировки данных, которые минимизируют риск утечки этих данных через логи.

Чтобы понять, как данные проходят через систему, мы создаем Data Flow диаграммы. Эти схемы визуализируют весь путь, по которому движутся чувствительные данные: от момента их получения до сохранения или обработки. Например, пароли, поступающие на сервер, никогда не хранятся в открытом виде. Сразу после получения они хэшируются. Логирование в этом случае проводится с маскированием всех чувствительных данных, чтобы исключить их утечку даже при доступе к журналам событий. 

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

Микросервисная архитектура

Сегодня разработчики все чаще переходят от монолитных систем к микросервисам и контейнерам (Docker/Kubernetes). Поэтому необходимо учитывать не только уязвимости самих микросервисов, но и обеспечивать безопасность их взаимодействия и запросов даже в пределах внутренней инфраструктуры. 

Здесь мы используем защищенные каналы связи между микросервисами, даже если они работают в изолированном контуре. Шифрование и строгая авторизация запросов с использованием, например, токенов JWT, — это не опция, а обязательный стандарт. Важно убедиться, что токены подписаны с использованием шифрования. Если это игнорировать, злоумышленники могут подделывать токены или обходить проверки.

def decode(jwt_token: str, rs256_public_key: str) -> dict:

return jwt.decode (

jwt_token, # Токен JWT

rs256_public_key, # Публичный ключ, от сервиса, который выпустил algorithms=["RS256"], # Обязательно указываем допустимый алгоритм,

# чтобы закрыть уязвимость использования "none"

audience="service_audience", # Сервис, для которого выпущен токен issuer="service_issuer" # Сервис, который выпустил токен

)

Далее переходим к защите самого микросервиса. Рассматривая маршрутизацию, можно выделить возможности K8s Gateway API в качестве гибкой альтернативы стандартному ingress-контроллеру. Но Gateway API не равно API Gateway. У Gateway API есть инструменты для интеллектуальной маршрутизации запросов. С помощью них можно направлять авторизованных пользователей на основные сервисы, а неавторизованных — на ресурсы, предназначенные исключительно для аутентификации. 

apiVersion: gateway.networking.k8s.io/v1

kind: HTTPRoute 

metadata:

name: api-gateway-example

spec:

rules:

-matches: 

headers:

-name: "Authorization" # если пользователь авторизован

backendRefs:

- name: api-service # направим на обработку в реальный сервис

port: 8080

-matches:

-path:

type: PathPrefix

value: /v1

backendRefs:

- name: login-service # в ином случае на сервис авторизации

port: 8090

Благодаря этому подходу мы, к примеру, разгружаем основные системы во время пиковых нагрузок, таких как массовая авторизация, и сохраняем стабильность для действующих пользователей.

Заключение

Какой же мы получили от этого профит? Применяя все эти практики комплексно, мы смогли снизить количество однотипных уязвимостей в нашем коде почти в 10 раз — по показателю багов на тысячу строк кода. Кроме того, на 70% сократилось время на code review в области безопасности, потому что многие стандартные проверки разработчики научились выполнять заранее. А крупные проекты по созданию новых сервисов пошли быстрее примерно на 20%, ведь теперь команды ориентируются на четко прописанные принципы и инфраструктурные решения.

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

Комментарии (0)