Перейти к содержанию

SQL-инъекция в ContentProvider

Описание

Что такое Content Provider?

Как следует из названия, Content Provider отвечает за предоставление данных и управление доступом к ним. Эти данные могут храниться в файловой системе, в базе данных SQLite или в любом другом постоянном хранилище, доступном из приложения. С помощью Content Provider любое приложение с соответствующими разрешениями может читать или даже изменять данные, используя стандартный набор API, который позволяет выполнять необходимые транзакции.

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

SQL-инъекция — это уязвимость, основанная на внедрении в SQL-запрос произвольного SQL-кода. Как правило, это позволяет злоумышленнику просматривать данные, которые он обычно не может получить. Атака может раскрыть личные данные, повредить содержимое базы данных и даже скомпрометировать внутреннюю инфраструктуру. Обычно SQL-инъекция реализуется через запросы, которые создаются динамически — путем объединения пользовательского ввода перед выполнением SQL-запроса.

В этом базовом примере неэкранированный ввод пользователя в поле номера заказа может быть вставлен в строку SQL и интерпретирован как следующий запрос.

SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1
Замену номера заказа на OR 1=1 база данных оценит как условие True, поскольку единица всегда равна единице.

Точно так же следующий запрос возвращает все строки из таблицы.

SELECT * FROM purchases WHERE email='admin@app.com' OR 1=1;

Стоит отметить, что не все SQL-инъекции приводят к потенциальным проблемам. Некоторые Content Provider уже предоставляют читателям полный доступ к базе данных SQLite; возможность выполнять произвольные запросы дает небольшое преимущество. А вот реализации, которые могут представлять проблему безопасности:

  • Несколько Content Provider совместно используют один файл базы данных SQLite. Успешная SQL-инъекция в одном контент-провайдере предоставит доступ к любым другим таблицам.

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

Влияние

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

Дополнительная информация

Projection: позволяет выбирать столбцы таблицы, к которой мы обращаемся.

Selection: позволяет использовать условие для выбора строк таблицы, к которой мы обращаемся.

С помощью этого запроса мы хотим получить поля: имя, размер и формат (столбцы — projection) всех записей (строки — selection), размер которых больше 20.

Рекомендации

  1. Используйте заменяемые параметры. Используя ? как заменяемый параметр в selection clauses и отдельный массив selection, мы связываем пользовательский ввод непосредственно с запросом и он не интерпретируется как часть инструкции SQL.

    // Формирование selection с заменяемыми параметрами.
    String selectionClause =  "var = ?";
    // Создание массива аргументов.
    String[] selectionArgs = {""};
    // Добавляем значения в массив с selection arguments.
    selectionArgs[0] = userInput;
    

    Пользовательский ввод привязывается непосредственно к запросу, а не обрабатывается как SQL, что предотвращает внедрение кода.

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

    public boolean validateOrderDetails(String email, String orderNumber) {
        boolean bool = false;
        Cursor cursor = db.rawQuery(
            "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?", 
            new String[]{email, orderNumber});
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                bool = true;
            }
            cursor.close();
        }
        return bool;
    }
    
  2. Используйте объекты PreparedStatement. Интерфейс PreparedStatement предварительно компилирует операторы SQL как объект, который затем может эффективно выполняться несколько раз. PreparedStatement использует ? в качестве заполнителя для параметров, которые сделают следующую скомпилированную попытку внедрения неэффективной.

    WHERE id=295094 OR 1=1;
    

    В этом случае выражение 295094 OR 1=1 считывается как значение для id, тогда как необработанный запрос будет интерпретировать оператор OR 1=1 как другую часть условия WHERE. В приведенном ниже примере показан параметризованный запрос.

    PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?");
    pstmt.setString(1, "Barista")   
    pstmt.setInt(2, 295094)
    
  3. Используйте query методы. В этом более длинном примере аргументы selection и selectionArgs метода query() объединены в предложение WHERE. Поскольку аргументы предоставляются отдельно, они экранируются перед их комбинацией, что предотвращает внедрение SQL.

    SQLiteDatabase db = dbHelper.getReadableDatabase();
    // Формируем projection, который определяет какие столбцы мы выбираем из базы данных.
    String[] projection = {
        BaseColumns._ID,
        FeedEntry.COLUMN_NAME_TITLE,
        FeedEntry.COLUMN_NAME_SUBTITLE
    };
    
    // Фильтруем результат WHERE "title" = 'My Title'.
    String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
    String[] selectionArgs = { "My Title" };
    
    // Определяем, как сортировать результат в Cursor object.
    String sortOrder =
        FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";
    
    Cursor cursor = db.query(
        FeedEntry.TABLE_NAME,   // Таблица
        projection,             // Массив столбцов, которые не нужны (передаём null чтобы получить все столбцы)
        selection,              // Столбцы для WHERE условия
        selectionArgs,          // Значения для WHERE условия
        null,                   // Строки не группировать
        null,                   // Не фильтровать по группам строк
        sortOrder               // Порядок сортировки
        );
    
  4. Используйте правильно сконфигурированный SQLiteQueryBuilder. Разработчики могут дополнительно защитить приложения с помощью SQLiteQueryBuilder — класса, который помогает создавать запросы для отправки в SQLiteDatabase. Рекомендуемые конфигурации включают:

    • Режим setStrict() для проверки запроса.
    • setStrictColumns() для проверки того, что столбцы включены в список разрешенных в setProjectionMap.
    • setStrictGrammar() для ограничения подзапросов.
  5. Используйте Room библиотеку. Пакет android.database.sqlite предоставляет API, необходимые для использования баз данных на Android. Однако этот подход требует написания низкоуровневого кода и не требует проверки необработанных SQL-запросов во время компиляции. По мере изменения данных затронутые SQL-запросы необходимо обновлять вручную — процесс, требующий много времени и подверженный ошибкам.

    Решением является использование библиотеки Room Persistence Library в качестве уровня абстракции для баз данных SQLite. Room library включают:

    • Класс базы данных, который служит основной точкой доступа для подключения к данным приложения.
    • Data entities, представляющие таблицы базы данных.
    • Data access objects (DAOs), предоставляющие методы, которые приложение может использовать для запроса, обновления, вставки и удаления данных.

    К преимуществам Room library относятся:

    • Проверка SQL-запросов во время компиляции.
    • Сокращение подверженного ошибкам шаблонного кода.
    • Оптимизированная миграция базы данных.
  6. Защищайте Content Provider, если необходимо, чтобы провайдер был экспортируемым. Ограничивайте доступ к Content Provider с помощью атрибутов android:permission, android:readPermission или android:writePermission, или предоставляйте доступ только к конкретным uri с помощью <grant-uri-permission …/>, либо <path-permission.….

    А также, если у Content Provider есть атрибуты android:readPermission и android:writePermission одновременно, то не рекомендуется использовать одно и то же разрешение, чтобы, например, приложения, которым необходимо только чтение, не получали дополнительную возможность на запись, и наоборот.

Ссылки

  1. SQL injections
  2. SQL injection in Android content providers and how to be protected
К началу