SQL Injections [SQLi]

1. Введение:

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

  1. Прекратить писать динамические запросы для своих приложений.

  2. Фильтровать и экранировать вводимые пользователем данные.

Представленные здесь приемы технически применимы к большинству других языков программирования и/или типов баз данных.

2. Типичный уязвимый код:

function authenticate(req, res, next) {
    var email = req.query.email,
        password = req.query.password,
        sqlRequest = new sql.Request(),
        sqlQuery = "select * from users where (email='" 
        + "' and password = '" + password + "')";
    
    sqlRequest.query(sqlQuery)
        .then(function (recordset) {
            if (recordset.length == 1) {
                loggedIn = true;
                // Auth successful
            } else {
                // Auth failure
            }
        })
        .catch(next);
}

sqlQuery Выполняет SQL-запрос без какой-либо проверки ввода. (т. е. не проверяется наличие легальных символов, минимальная/максимальная длина строки или удаление "вредоносных" символов).

Злоумышленник может ввести необработанный синтаксис SQL в поля ввода имени пользователя и пароля, чтобы изменить значение SQL-запроса, отвечающего за аутентификацию, что приведет к обходу механизма аутентификации приложения. Примером такого обхода может быть запрос: ' or 1=1)#

3. Смягчение последствий:

Атаки SQL Injection, к сожалению, очень распространены, и это объясняется двумя факторами:

  1. значительной распространенностью уязвимостей SQL Injection и

  2. Привлекательность цели (т.е. база данных обычно содержит все интересные/критичные данные для вашего приложения).

Довольно стыдно, что до сих пор существует так много успешных SQLi, потому что на самом деле очень просто избежать этого типа уязвимостей. В этой статье вы узнаете о 4 самых надежных методах разработки надежного и безопасного приложения.

3.1. Подготовленные условия:

Использование подготовленных операторов с привязкой к переменным (они же параметризованные запросы) - это то, как все разработчики должны писать свои запросы с самого начала. Они просты в написании и более понятны, чем динамические запросы. Параметризованные запросы заставляют разработчика сначала определить весь SQL-код, а затем передать каждый параметр в запрос. Такой стиль кодирования позволяет базе данных различать код и данные, независимо от того, что вводит пользователь.

Подготовленные операторы гарантируют, что злоумышленник не сможет изменить смысл запроса, даже если SQL-команды будут вставлены злоумышленником. В приведенном ниже безопасном примере, если бы злоумышленник ввел сообщение электронной почты bobi@mail.com' или '1'='1, параметризованный запрос не был бы уязвимым и вместо этого искал бы имя пользователя, которое буквально соответствовало бы всей строке bobi@mail.com' или '1'='1.

function authenticate(req, res, next) {
    var email = req.query.email,
        password = req.query.password,
        ps = new sql.PreparedStatement(),
        sqlQuery = "select * from users where (email = @email and " 
                 + "password = @password)";
    
    ps.input('email', sql.VarChar(50));
    ps.input('password', sql.VarChar(50));
    ps.prepare(sqlQuery)
    .then(function () {
        return ps.execute({email: email, password: password})
        then(function (recordset) {
            if (recordset.length == 1) {
                loggedIn = true;
                // Auth successful
    
            } else {
                // Auth failure
            }
        })
    })
    .catch(next);
};

3.2. Хранимые процедуры:

Хранимые процедуры не всегда безопасны для SQLi. Однако некоторые стандартные конструкции программирования хранимых процедур имеют тот же эффект, что и использование параметризованных запросов при их безопасной реализации.

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

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

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

CREATE PROCEDURE SafeAuth(@username varchar(50),  @password varchar(50))
AS
BEGIN
DECLARE @sql varchar(150)
    SELECT Username,Password FROM dbo.Login
    WHERE Userame=@username AND Password=@password
end

Примечание: "Реализовано безопасно" означает, что хранимая процедура не содержит небезопасной динамической генерации SQL. Разработчики обычно не генерируют динамический SQL внутри хранимых процедур. Однако это может быть сделано, но этого следует избегать. Если этого нельзя избежать, хранимая процедура должна использовать проверку ввода или правильное экранирование, как описано в этой статье, чтобы убедиться, что все вводимые пользователем данные хранимой процедуры не могут быть использованы для внедрения SQL-кода в динамически генерируемый запрос.

Хотя хранимые процедуры являются эффективным способом защиты от SQLI, очень важно сочетать их с другими методами, такими как подготовленные операторы.

3.3. Проверка ввода:

Существует два типа проверки ввода: синтаксическая и семантическая.

Синтаксическая проверка обеспечивает синтаксическую корректность структурированных полей. (например, SSN, дата, символ валюты и т. д.).

Семантическая валидация обеспечивает корректность вводимых значений в конкретном бизнес-контексте. (например, дата начала до даты окончания, цена в пределах приписанного диапазона и т. д.)

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

Однако важно отметить, что любая проверка ввода JavaScript, выполняемая на стороне клиента, может быть обойдена злоумышленником, отключившим JavaScript или использующим веб-прокси. Убедитесь, что любая проверка ввода, выполняемая на стороне клиента, выполняется и на стороне сервера.

  • Валидаторы типов данных, имеющиеся во фреймворках веб-приложений (например, Django Validators, Apache Commons Validators и др.).

  • Валидация по JSON Schema и XML Schema (XSD) для ввода данных в этих форматах.

  • Преобразование типов (например, Integer.parseInt() в Java, int() в Python) со строгой обработкой исключений.

  • Проверка минимального и максимального диапазона значений для числовых параметров и дат, проверка минимальной и максимальной длины для строк.

  • Массив допустимых значений для небольших наборов строковых параметров (например, дней недели).

  • Регулярные выражения для любых других структурированных данных, охватывающие всю входную строку (^...$) и не использующие подстановочный знак "любой символ" (например, . или \S). Разработка регулярных выражений может быть сложной, поэтому при разработке такой проверки рекомендуется руководствоваться комплексным ресурсом.

3.4. Исключение вводимых данных пользователем:

Этот метод следует использовать только в крайнем случае, когда ни один из вышеперечисленных способов не подходит. Вероятно, лучшим выбором будет проверка ввода, поскольку данная методология хрупка и мы не можем гарантировать, что она предотвратит все SQLi во всех ситуациях.

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

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

var SqlString = require('sqlstring');

var userId = 1;
var sql = SqlString.format('SELECT * FROM users WHERE id = ?', [userId]);
console.log(sql); // SELECT * FROM users WHERE id = 1

4. Выводы:

SQLI НЕ ДОЛЖЕН СУЩЕСТВОВАТЬ! Так говорят все. И они правы! Если бы каждый разработчик:

  • Подготовил SQL-запросы.

  • Валидировал и экранировал пользовательский ввод.

  • Писал чистый эффективный код.( =D )

Last updated