Топ-10 уязвимостей безопасности в Java: теория и практика

Курс посвящен детальному разбору самых критических уязвимостей Java-приложений, основанных на рейтинге OWASP Top 10. Студенты изучат примеры уязвимого кода, механизмы эксплуатации и современные методы защиты и исправления ошибок.

1. Внедрение кода: SQL-инъекции и инъекции команд в Java-приложениях

Внедрение кода: SQL-инъекции и инъекции команд в Java-приложениях

Добро пожаловать на курс «Топ-10 уязвимостей безопасности в Java: теория и практика». Мы начинаем наше погружение в мир кибербезопасности с одной из самых старых, но всё ещё разрушительных категорий уязвимостей — инъекций (Injection).

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

Что такое инъекция?

Инъекция происходит, когда ненадежные данные (ввод пользователя) отправляются интерпретатору как часть команды или запроса. Проблема заключается в том, что интерпретатор (будь то база данных, командная оболочка ОС или парсер XML) не может отличить данные от исполняемого кода.

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

В Java-мире наиболее распространены два типа инъекций:

  • SQL Injection (SQLi) — атака на базу данных.
  • Command Injection — атака на операционную систему сервера.
  • Разберем их детально.

    SQL-инъекции (SQL Injection)

    SQL-инъекция возникает, когда разработчик создает SQL-запрос путем простой конкатенации (склеивания) строк, включая в запрос данные, полученные от пользователя, без предварительной проверки или экранирования.

    Анатомия уязвимости

    Представьте, что у вас есть простой метод аутентификации, использующий JDBC:

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

    SELECT * FROM users WHERE name = 'admin'

    Однако, что если злоумышленник введет в поле username следующую строку:

    admin' OR '1'='1

    В результате конкатенации итоговый SQL-запрос, отправленный в базу данных, примет вид:

    SELECT * FROM users WHERE name = 'admin' OR '1'='1'

    Математическая логика атаки

    Давайте посмотрим на это с точки зрения булевой алгебры. Условие WHERE теперь состоит из двух частей, объединенных оператором ИЛИ (OR).

    Запишем это в виде формулы:

    где — это условие проверки имени пользователя (name = 'admin'), — это внедренное условие ('1'='1'), а — логический оператор ИЛИ.

    В логике существует правило: если хотя бы один из операндов в операции ИЛИ истинен, то и все выражение истинно. Так как выражение '1'='1' всегда истинно (является тавтологией), то и все условие WHERE становится истинным для каждой строки в таблице users. В результате база данных вернет первую попавшуюся запись (часто это запись администратора), и злоумышленник войдет в систему без пароля.

    Последствия SQLi

    * Обход аутентификации: Вход под чужим аккаунтом. * Утечка данных: Использование оператора UNION для извлечения данных из других таблиц. * Потеря данных: Злоумышленник может выполнить DROP TABLE или DELETE.

    Защита: Prepared Statements

    Единственный надежный способ защиты от SQL-инъекций в Java — использование параметризованных запросов (Prepared Statements).

    Вместо того чтобы вклеивать данные в строку запроса, мы используем плейсхолдеры (знаки вопроса ?).

    Почему это работает?

    Когда вы используете PreparedStatement, база данных сначала компилирует структуру SQL-запроса, и только потом подставляет данные. База данных «знает», что то, что придет вместо ?, — это просто данные, строка текста, а не исполняемая команда SQL. Даже если отправить admin' OR '1'='1, база будет искать пользователя с буквальным именем «admin' OR '1'='1».

    Инъекции команд (Command Injection)

    Инъекция команд (или OS Command Injection) происходит, когда приложение передает небезопасные данные пользователя в системную оболочку (shell) для выполнения команд операционной системы.

    В Java это часто случается при использовании классов Runtime или ProcessBuilder.

    Пример уязвимого кода

    Допустим, вы пишете утилиту для администраторов, которая проверяет доступность сервера через команду ping.

    Разработчик ожидает, что в address придет IP-адрес, например 8.8.8.8.

    Но злоумышленник может передать такую строку:

    8.8.8.8; rm -rf /

    (В Windows аналогом разделителя ; может быть & или |).

    В результате операционная система получит команду:

    ping -c 3 8.8.8.8; rm -rf /

    Сначала выполнится ping, а сразу после него — команда удаления файлов rm -rf /. Если приложение запущено с правами root (что само по себе плохая практика), последствия будут катастрофическими.

    Защита от Command Injection

  • Избегайте вызова системных команд. В 99% случаев в Java есть нативный API для выполнения задачи. Например, вместо вызова ls используйте java.nio.file.Files.
  • Используйте ProcessBuilder с разделением аргументов. Не передавайте всю команду одной строкой.
  • Безопасный вариант:

    В этом случае ProcessBuilder не запускает оболочку (shell) для интерпретации строки. Он вызывает исполняемый файл ping и передает ему address как один аргумент. Даже если в address будут спецсимволы ; или &&, программа ping просто попытается найти хост с таким странным именем и выдаст ошибку «Unknown host», но вторая команда не выполнится.

  • Валидация ввода. Используйте строгие белые списки (allow-lists). Если вы ожидаете IP-адрес, убедитесь, что ввод соответствует формату IPv4 или IPv6, используя регулярные выражения или специальные библиотеки.
  • Общие принципы защиты

    Независимо от типа инъекции, следуйте этим правилам:

    * Никогда не доверяйте вводу пользователя. Любые данные извне (формы, заголовки HTTP, файлы) потенциально опасны. * Разделяйте данные и код. Это главный принцип защиты от инъекций. * Принцип наименьших привилегий. Приложение должно работать с минимально необходимыми правами в базе данных и ОС. Если приложению нужно только читать данные, у него не должно быть прав на DROP TABLE.

    > «Безопасность — это не продукт, а процесс.» — Брюс Шнайер

    В следующей статье мы рассмотрим Cross-Site Scripting (XSS) — ситуацию, когда инъекция происходит не на сервере, а в браузере пользователя, и узнаем, почему JavaScript может быть опаснее, чем кажется.

    2. Нарушение аутентификации и управления доступом: ошибки сессий и авторизации

    Нарушение аутентификации и управления доступом: ошибки сессий и авторизации

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

    Сегодня мы поговорим о двух фундаментальных столпах безопасности любого Java-приложения: Аутентификации (Authentication) и Авторизации (Authorization). Ошибки в этих механизмах (Broken Authentication и Broken Access Control) неизменно занимают верхние строчки в рейтинге OWASP Top 10.

    Аутентификация против Авторизации

    Прежде чем углубляться в уязвимости, давайте четко разграничим понятия. В Java-сообществе их часто путают.

    * Аутентификация (AuthN): Ответ на вопрос «Кто ты?». Это процесс проверки учетных данных (логин/пароль, биометрия, токен). * Авторизация (AuthZ): Ответ на вопрос «Что тебе можно делать?». Это проверка прав доступа уже аутентифицированного пользователя к конкретному ресурсу.

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

    Уязвимости управления сессиями (Broken Authentication)

    В протоколе HTTP нет состояния (stateless). Чтобы сервер «помнил» пользователя между запросами, используются сессии. В Java (Servlet API) это обычно реализуется через JSESSIONID.

    Фиксация сессии (Session Fixation)

    Это классическая атака, которая возможна, если приложение не обновляет идентификатор сессии после входа пользователя в систему.

    Сценарий атаки:

  • Злоумышленник заходит на сайт и получает валидный JSESSIONID (например, ABC123XYZ). Он еще не авторизован, но сессия уже создана сервером.
  • Злоумышленник отправляет жертве ссылку, содержащую этот идентификатор: http://example.com/login?jsessionid=ABC123XYZ.
  • Жертва переходит по ссылке и вводит свои логин и пароль.
  • Если сервер не выдаст новый идентификатор сессии после успешного входа, то сессия ABC123XYZ станет авторизованной.
  • Злоумышленник, зная этот ID, использует его в своем браузере и получает доступ к аккаунту жертвы.
  • [VISUALIZATION: Схема атаки Session Fixation. Слева хакер, справа жертва, посередине сервер. Хакер получает ID сессии от сервера, передает его жертве через ссылку. Жертва логинится на сервере с этим ID. Сервер помечает ID как "залогиненный". Хакер использует тот же ID для доступа к аккаунту. Стрелки показывают поток данных.]

    Как защититься в Java:

    Всегда меняйте идентификатор сессии после успешной аутентификации. В Spring Security это включено по умолчанию (стратегия migrateSession или changeSessionId), но при кастомной реализации это нужно делать вручную:

    Слабые политики паролей и отсутствие защиты от перебора

    Если ваше Java-приложение позволяет бесконечно вводить неверный пароль, оно уязвимо для Brute Force атак.

    Решение: Внедрите механизм блокировки (Rate Limiting) или временной блокировки аккаунта после неудачных попыток. Используйте библиотеки вроде Bucket4j для ограничения количества запросов.

    Нарушение контроля доступа (Broken Access Control)

    Даже если пользователь надежно аутентифицирован, это не значит, что ему можно доверять. Самая распространенная ошибка здесь — IDOR.

    Небезопасные прямые ссылки на объекты (IDOR)

    Insecure Direct Object References возникает, когда приложение предоставляет прямой доступ к объектам (файлам, записям в БД) на основе пользовательского ввода без проверки прав владения.

    Пример уязвимого кода (Spring Boot Controller):

    Злоумышленник просто меняет /documents/123 на /documents/124 и получает доступ к данным, которые ему не принадлежат. Это называется горизонтальным повышением привилегий.

    Математическая модель контроля доступа

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

    Пусть — множество субъектов (пользователей), — множество объектов (ресурсов), а — множество действий (чтение, запись).

    Функция доступа может быть описана так:

    Где: * — субъект, инициирующий запрос. * — объект, к которому запрашивается доступ. * — действие, которое субъект хочет совершить. * Результат означает «Разрешено», — «Запрещено».

    Ошибка IDOR возникает, когда разработчик забывает реализовать эту функцию и полагается только на наличие объекта в базе данных. Правильная проверка должна выглядеть так:

    Где: * — решение о доступе. * — тогда и только тогда. * — проверка того, что объект принадлежит области видимости пользователя (например, пользователь является владельцем документа). * — логическое И. * — проверка того, что роль пользователя достаточно высока для выполнения действия над объектом .

    Исправление IDOR в Java

    Чтобы исправить предыдущий пример, нам нужно проверить, принадлежит ли документ текущему пользователю:

    В Spring Security для этого удобно использовать аннотации @PreAuthorize с языком выражений SpEL:

    Отсутствие контроля доступа на уровне функций

    Иногда разработчики скрывают кнопки в интерфейсе (UI), думая, что этого достаточно.

    Например, кнопка «Удалить пользователя» скрыта для обычного юзера, но API-эндпоинт /api/admin/deleteUser остается доступным. Если злоумышленник угадает URL или подсмотрит его в JavaScript-коде, он сможет отправить запрос напрямую, минуя интерфейс. Это называется вертикальным повышением привилегий.

    Защита: Никогда не полагайтесь на скрытие элементов в UI. Все проверки должны дублироваться на сервере. В Spring Security используйте конфигурацию SecurityFilterChain:

    Лучшие практики для Java-разработчика

  • Используйте проверенные фреймворки. Не пишите свою криптографию и управление сессиями. Spring Security или Apache Shiro уже решили эти проблемы за вас.
  • Принцип наименьших привилегий. Пользователь должен иметь права только на те действия, которые необходимы для его работы.
  • Stateless архитектура (JWT). В современных микросервисах часто отказываются от сессий в пользу токенов (JWT). Это устраняет проблему фиксации сессии, но добавляет новые вызовы (хранение токенов, их отзыв).
  • Тестирование. Используйте инструменты вроде OWASP ZAP или Burp Suite, чтобы проверять свои API на наличие IDOR.
  • > «Безопасность — это не то, что вы покупаете, это то, что вы делаете.» — Неизвестный автор

    В следующей статье мы рассмотрим утечку конфиденциальных данных (Sensitive Data Exposure) и узнаем, почему хранить пароли в base64 — это плохая идея, и как правильно шифровать данные в Java.