SQL-инъекция

Категория OWASP: MASVS-CODE: Качество кода

Обзор

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

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

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

SELECT * FROM users WHERE email = 'example@example.com' AND order_number = '251542'' LIMIT 1

Подобный код вызовет ошибку синтаксиса базы данных в веб-консоли, что укажет на потенциальную уязвимость приложения для SQL-инъекций. Замена порядкового номера на 'OR 1=1– означает, что аутентификация возможна, поскольку база данных оценивает это утверждение как True , так как единица всегда равна единице.

Аналогичным образом, этот запрос возвращает все строки из таблицы:

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

Поставщики контента

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

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

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

Влияние

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

Меры по смягчению последствий

Заменяемые параметры

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

Котлин

// Constructs a selection clause with a replaceable parameter.
val selectionClause = "var = ?"

// Sets up an array of arguments.
val selectionArgs: Array<String> = arrayOf("")

// Adds values to the selection arguments array.
selectionArgs[0] = userInput

Java

// Constructs a selection clause with a replaceable parameter.
String selectionClause =  "var = ?";

// Sets up an array of arguments.
String[] selectionArgs = {""};

// Adds values to the selection arguments array.
selectionArgs[0] = userInput;

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

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

Котлин

fun validateOrderDetails(email: String, orderNumber: String): Boolean {
    val cursor = db.rawQuery(
        "select * from purchases where EMAIL = ? and ORDER_NUMBER = ?",
        arrayOf(email, orderNumber)
    )

    val bool = cursor?.moveToFirst() ?: false
    cursor?.close()

    return bool
}

Java

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;
}

Используйте объекты PreparedStatement

Интерфейс PreparedStatement предварительно компилирует SQL-запросы в объект, который затем может эффективно выполняться многократно. PreparedStatement использует знак вопроса ? в качестве заполнителя для параметров, что сделает следующую попытку внедрения кода неэффективной:

WHERE id=295094 OR 1=1;

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

Котлин

val pstmt: PreparedStatement = con.prepareStatement(
        "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?").apply {
    setString(1, "Barista")
    setInt(2, 295094)
}

Java

PreparedStatement pstmt = con.prepareStatement(
                                "UPDATE EMPLOYEES SET ROLE = ? WHERE ID = ?");
pstmt.setString(1, "Barista")   
pstmt.setInt(2, 295094)

Используйте методы запросов

В этом более подробном примере параметры selection и selectionArgs метода query() объединены для создания условия WHERE . Поскольку аргументы предоставляются отдельно, они экранируются перед их объединением, что предотвращает SQL-инъекции.

Котлин

val db: SQLiteDatabase = dbHelper.getReadableDatabase()
// Defines a projection that specifies which columns from the database
// should be selected.
val projection = arrayOf(
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
)

// Filters results WHERE "title" = 'My Title'.
val selection: String = FeedEntry.COLUMN_NAME_TITLE.toString() + " = ?"
val selectionArgs = arrayOf("My Title")

// Specifies how to sort the results in the returned Cursor object.
val sortOrder: String = FeedEntry.COLUMN_NAME_SUBTITLE.toString() + " DESC"

val cursor = db.query(
    FeedEntry.TABLE_NAME,  // The table to query
    projection,            // The array of columns to return
                           //   (pass null to get all)
    selection,             // The columns for the WHERE clause
    selectionArgs,         // The values for the WHERE clause
    null,                  // Don't group the rows
    null,                  // Don't filter by row groups
    sortOrder              // The sort order
).use {
    // Perform operations on the query result here.
    it.moveToFirst()
}

Java

SQLiteDatabase db = dbHelper.getReadableDatabase();
// Defines a projection that specifies which columns from the database
// should be selected.
String[] projection = {
    BaseColumns._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
};

// Filters results WHERE "title" = 'My Title'.
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// Specifies how to sort the results in the returned Cursor object.
String sortOrder =
    FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,   // The table to query
    projection,             // The array of columns to return (pass null to get all)
    selection,              // The columns for the WHERE clause
    selectionArgs,          // The values for the WHERE clause
    null,                   // don't group the rows
    null,                   // don't filter by row groups
    sortOrder               // The sort order
    );

Используйте правильно настроенный SQLiteQueryBuilder.

Разработчики могут дополнительно защитить приложения, используя SQLiteQueryBuilder — класс, который помогает создавать запросы для отправки объектам SQLiteDatabase . Рекомендуемые конфигурации включают:

  • Режим setStrict() для проверки запросов.
  • Используйте setStrictColumns() для проверки того, что столбцы включены в список разрешенных в setProjectionMap.
  • setStrictGrammar() для ограничения количества подзапросов.

Библиотека «Комната для пользования»

Пакет android.database.sqlite предоставляет API, необходимые для использования баз данных на Android. Однако такой подход требует написания низкоуровневого кода и не предусматривает проверки SQL-запросов на этапе компиляции. По мере изменения графов данных, соответствующие SQL-запросы необходимо обновлять вручную — это трудоемкий и подверженный ошибкам процесс.

Одно из наиболее эффективных решений — использование библиотеки Room Persistence Library в качестве абстрактного слоя для баз данных SQLite. Функционал Room включает в себя:

  • Класс базы данных, служащий основной точкой доступа для подключения к сохраненным данным приложения.
  • Субъекты данных, представляющие таблицы базы данных.
  • Объекты доступа к данным (DAO) предоставляют приложениям методы для запроса, обновления, вставки и удаления данных.

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

  • Проверка SQL-запросов на этапе компиляции.
  • Сокращение количества подверженного ошибкам шаблонного кода.
  • Упрощенная миграция базы данных.

Передовые методы

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

  • Надежные, односторонние и «соленые» хэши для шифрования паролей:
    • 256-битное AES для коммерческого применения.
    • Размер открытого ключа для криптографии на эллиптических кривых составляет 224 или 256 бит.
  • Ограничение прав доступа.
  • Точное структурирование форматов данных и проверка соответствия данных ожидаемому формату.
  • По возможности следует избегать хранения личных или конфиденциальных данных пользователей (например, реализовывать логику приложения путем хеширования, а не передачи или хранения данных).
  • Минимизация использования API и сторонних приложений, имеющих доступ к конфиденциальным данным.

Ресурсы