Категория 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 и сторонних приложений, которые получают доступ к конфиденциальным данным.