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

Ява

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

Ява

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

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

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

WHERE id=295094 OR 1=1;

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

Котлин

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

Ява

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()
}

Ява

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 в качестве уровня абстракции для баз данных SQLite. К особенностям номера относятся:

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

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

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

Лучшие практики

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

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

Ресурсы