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