Как стать автором
Поиск
Написать публикацию
Обновить
326.55
PVS-Studio
Статический анализ кода для C, C++, C# и Java

Пользовательские аннотации PVS-Studio теперь и в Java

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров451

Начиная с версии PVS-Studio 7.38, Java анализатор вслед за двумя братьями C# и C++ поддерживает пользовательские аннотации в формате JSON. Зачем они нужны и что с ними можно делать, рассмотрим в этой статье.

Какие аннотации?

Аннотации (или метаданные) появились в Java ещё в версии 5. Они были созданы для того, чтобы представить альтернативу созданию "побочных файлов" для работы с различными API.

Прежний подход до сих пор можно наблюдать в Spring Framework, например, при объявлении бинов через .xml файлы. А вот в Spring Boot, как известно, аннотации находятся непосредственно в коде.

По иронии судьбы пользовательские аннотации в PVS-Studio реализованы посредством JSON файлов. Круг замкнулся?

Причины такого решения описаны в статье моего коллеги. В Java анализаторе к этому списку причин также добавляется поддержание общего формата.

Что разметить?

С аннотациями для инструментов статического анализатора знаком каждый Java разработчик. Классический пример — многочисленные версии @Nullable и @NotNull. Эти аннотации не только документируют поведение кода, но и легко учитываются при его анализе.

Полезность аннотаций особенно заметна в среде разработки IntelliJ IDEA, которая использует их для генерации предупреждений. Разумеется, PVS-Studio также умеет их воспринимать.

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

Таким образом, аннотации могут позволить тонко настроить статический анализатор. А в результате этой настройки разработчик получает:

  • большее количество полезных срабатываний;

  • меньшее количество ложноположительных.

Как это сделать в PVS-Studio, можно посмотреть по ссылке. Отмечу, что на момент написания статьи были поддержаны исключительно taint-аннотации, т.е. аннотации для диагностик, выявляющих использование непроверенных данных. Например, SQL-инъекции. Полный список этих диагностик указан в той же документации.

Поддержка такой разметки также необходима для ГОСТ Р 71207-2024, что требует от инструментов статического анализа наличия возможности размечать источники и приёмники чувствительных данных.

Соответствующие выдержки из ГОСТ Р 71207-2024.

п. 3.1.3 анализ помеченных данных: Статический анализ, при котором анализируется течение

потока данных от источников до стоков.

Примечания

  1. Под источниками понимаются точки программы, в которых данные начинают иметь пометку — некоторое заданное свойство. Под стоками понимаются точки программы, в которых данные перестают иметь пометку.

  2. Распространённая цель анализа помеченных данных — показать, что помеченные данные не могут попасть из источников — точек ввода пользователя в стоки — процедуры записи на диск или в сеть. Факт такого попадания означает утечку конфиденциальных данных.

п.7.6. Если статический анализатор для поиска ошибок, определённых в 6.3, перечисление а), применяет анализ помеченных данных, должна быть предоставлена возможность конфигурации анализа: должны задаваться процедуры-источники и процедуры-стоки чувствительных данных.

Теперь к примеру

Посмотрим пример кода и попробуем в нём что-то разметить. Источником внешних данных считаем метод DataSource.getData(). А также предлагаю ответить на вопрос, если ли в этом примере уязвимость.

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

package org.example;

import java.sql.DriverManager;

public class Main {
    public static void main(String[] args) {
        var db = "jdbc:sqlite:user.db";
        try (var connection = DriverManager.getConnection(db)) {
            var dao = new AuthInfoDAO(connection);
            var service = new AuthService(dao);
            var source = new DataSource();
            var username = source.getData("username");
            var password = source.getData("password");

            var loginSuccess = service.authenticate(username, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

AuthInfo.java — объект данных аутентификации.

package org.example;

public record AuthInfo(String name, String passwordHash) {}

При работе с базой данных будем считать, что таблица уже существует, и просто вытаскиваем из неё данные.

AuthInfoDAO.java — получение пользователя из базы данных.

package org.example;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;

public class AuthInfoDAO {
    private final Connection connection;

    public AuthInfoDAO(Connection connection) {
        this.connection = connection;
    }

    public AuthInfo findByUsername(String username) throws SQLException {
        var sql = "SELECT username, password_hash " +
                "FROM users WHERE username = ?";
        try (var stmt = connection.prepareStatement(sql)) {
            stmt.setString(1, username);
            ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                return new AuthInfo(
                        rs.getString("username"),
                        rs.getString("password_hash")
                );
            }
        }

        return null;
    }
}

Для авторизации пользователя будем создавать хэш с помощью BCypt и сравним его с тем, что получили из БД (если получили).

AuthService.java — логика аутентификации со сравнением хэшей пароля.

package org.example;

import org.mindrot.jbcrypt.BCrypt;

import java.sql.SQLException;

public class AuthService {
  private final AuthInfoDAO userInfoDao;

  public AuthService(AuthInfoDAO userInfoDao) {
    this.userInfoDao = userInfoDao;
  }

  public boolean authenticate(String name, String password) 
                              throws SQLException {
    var user = userInfoDao.findByUsername(name);
    if (user == null) {
        return false;
    }

    return BCrypt.checkpw(password, user.passwordHash());
  }
}

DataSource.java — некоторый источник данных. Реальная логика получения данных из такого источника нас не интересует. Как раз единственный метод этого класса мы затем аннотируем.

package org.example;

import java.util.HashMap;
import java.util.Map;

public class DataSource {
    private final Map<String, String> map = new HashMap<>();

    public String getData(String key) {
        // synthetic example source of external data
        return map.get(key);
    }
}

Файл аннотаций для метода getData(String key) будет выглядеть следующим образом:

{
  "language": "java",
  "version": 1,
  "annotations": [
    {
      "type": "method",
      "package": "org.example",
      "type_name": "DataSource",
      "method_name": "getData",
      "params": [
        {
          "package": "java.lang",
          "type_name": "String"
        }
      ],
      "returns": {
        "attributes": [
          "common_source"
        ]
      }
    }
  ]
}

Идея аннотации простая: возвращаемое значение метода является common_source или общим источником внешних данных. Общее, поскольку существуют и более конкретные аннотации, например, web_source. Такое разделение важно для некоторых диагностик, которым не следует ругаться на любой источник, а, например, исключительно на данные из веб-запросов.

Итак, мы разметили источник. Запустили анализатор, после чего настало время получить ответ на вопрос, станет ли он ругаться на что-либо. Ответ: не станет. SQL-запрос является параметризированным, соответственно, и угрозы инъекции нет. Получается, что благодаря разметке мы ничего не получили и этим заниматься не стоит? Едва ли. Рассмотрим сценарий дальнейшей работы над этим кодом.

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

package org.example;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.DriverManager;

public class Main {
  private static final Logger LOGGER = LogManager.getLogger();

  public static void main(String[] args) {
    var db = "jdbc:sqlite:user.db";
    try (var connection = DriverManager.getConnection(db)) {
      var dao = new AuthInfoDAO(connection);
      var service = new AuthService(dao);
      var source = new DataSource();
      var username = source.getData("username");
      var password = source.getData("password");

      LOGGER.info("Logging in user {}", username);
      var loginSuccess = service.authorize(username, password);
      var successMessage = loginSuccess ? "Success" : "Invalid credentials";
      LOGGER.info("Login: {}", successMessage);
    } catch (Exception e) {
      LOGGER.error(e);
    }
  }
}

Ничего необычного. Однако поскольку разработчик, который изначально работал над этим фрагментом, разметил метод getData, уже здесь анализатор PVS-Studio выдаст предупреждение:

V5319 Possible log injection. Potentially tainted data in the 'username' variable is written into logs. Main.java 20

Что такого может ввести пользователь, что это окажется проблемой? Например, при значении username admin\nINFO - Success\nINFO - Logging in user legit мы получаем логи:

INFO  - Logging in user admin
INFO  - Success
INFO  - Logging in user legit
INFO  - Login: Invalid credentials

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

Log injection был выбран как самый простой и достаточно реалистичный пример. Вместо добавления простого логирования, добавление функционала может привести к появлению нового пути исполнения, по которому данные попадают в совершенно другой сток.

Заключение

В примере можно заметить, что логгер мы не размечали. Всё потому, что все крупные библиотеки уже размечены в PVS-Studio. Следовательно, проверить свой проект на различные уязвимости можно уже сейчас, скачав анализатор здесь. И не надо вспоминать список всех методов, которые необходимо разметить, чтобы получить какой-то результат.

На этом завершается обзорная экскурсия о пользовательских аннотациях в Java анализаторе. Также можно прочитать аналогичные статьи про C++ и C#, если интересны примеры для этих языков.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Evgenii Slepyshkov. PVS-Studio user annotations are now in Java.

Теги:
Хабы:
+8
Комментарии0

Публикации

Информация

Сайт
pvs-studio.ru
Дата регистрации
Дата основания
2008
Численность
51–100 человек
Местоположение
Россия
Представитель
Андрей Карпов