SQL-инъекция

Применимо к:SQL ServerБаза данных Azure SQLУправляемый экземпляр Azure SQLAzure Synapse AnalyticsСистема платформы аналитики (PDW)Конечная точка SQL аналитики в Microsoft FabricХранилище в Microsoft FabricБаза данных SQL в Microsoft Fabric

SQL-инъекция — это атака, при которой вредоносный код внедряется в строки, которые затем передаются экземпляру ядра СУБД SQL Server для синтаксического разбора и выполнения. Любая процедура, которая создает инструкции SQL, должна проверяться для уязвимостей внедрения, так как ядро СУБД выполняет все синтаксически допустимые запросы, которые он получает. Даже параметризованные данные могут стать предметом манипуляций опытного злоумышленника.

Как работает SQL-инъекция

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

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

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

var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

Пользователю выводится запрос на ввод названия города. Если они вводят Redmond, запрос, собранный скриптом, выглядит следующим образом:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

Однако предположим, что пользователь вводит следующий текст:

Redmond';drop table OrdersTable--

В этом случае скрипт собирает следующий запрос:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

Точка с запятой (;) обозначает конец одного запроса и начало другого. Двойной дефис (--) указывает, что остальная часть текущей строки является комментарием и должна игнорироваться. Если измененный код синтаксически прав, он выполняется сервером. Когда ядро СУБД обрабатывает эту инструкцию, оно сначала выбирает все записи в OrdersTable, в которых значение ShipCity равно Redmond. Затем компонент ядро СУБД удаляет OrdersTable.

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

Проверка всех входных данных

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

  • Не делайте никаких предположений о размере, типе или содержимом данных, получаемых приложением. Например, рекомендуется оценить следующее.

    • Как ваше приложение ведет себя, если ошибившийся или злонамеренный пользователь вводит видеофайл размером 2 ГБ в поле, где приложение ожидает почтовый индекс?

    • Как ведёт себя ваше приложение, если в текстовом поле содержится оператор DROP TABLE?

  • Проверьте размер и тип вводимых данных и установите соответствующие ограничения. Это поможет предотвратить преднамеренное переполнение буфера.

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

  • При работе с XML-документами проверьте все данные по его схеме по мере ввода.

  • Никогда не создавайте инструкции Transact-SQL непосредственно из входных данных пользователя.

  • Для проверки вводимых пользователем данных используйте хранимые процедуры.

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

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

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

  • Никогда не сцепляйте входные данные пользователя, которые не проверяются. Конкатенация строк — основной способ внедрения скриптов.

  • Не принимайте следующие строки в полях, из которых можно сформировать имена файлов: AUX, CLOCK$, COM1 по COM8, CON, CONFIG$, LPT1 по LPT8, NUL и PRN.

Если это возможно, отклоните входные данные, содержащие следующие символы.

Входной символ Значение в языке Transact-SQL
; Разделитель запросов.
' Разделитель строк символьных данных.
-- Разделитель однострочного комментария. Текст, следующий -- до конца этой строки, не вычисляется сервером.
/*** ... ***/ Разделители комментариев. Текст между /* и */ не обрабатывается сервером.
xp_ Используется в начале имени расширенных хранимых процедур каталога, например xp_cmdshell.

Используйте типобезопасные параметры SQL

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

SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

В этом примере параметр @au_id обрабатывается как буквенное значение, а не исполняемый код. Это значение проверяется по типу и длине. Если значение @au_id не соответствует заданным ограничениям типа и длины, создается исключение.

Использование параметризованных входных данных с хранимыми процедурами

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

SqlDataAdapter myCommand =
    new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);

Если используются хранимые процедуры, то в качестве их входных данных следует использовать параметры.

Используйте коллекцию Parameters с динамическим SQL

Если вы не можете использовать хранимые процедуры, можно по-прежнему использовать параметры, как показано в следующем примере кода.

SqlDataAdapter myCommand = new SqlDataAdapter(
    "SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

Фильтрация входных данных

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

private string SafeSqlLiteral(string inputSQL)
{
    return inputSQL.Replace("'", "''");
}

Предложения LIKE

Если вы используете условие LIKE, подстановочные знаки по-прежнему должны быть экранированы:

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

Проверить код на наличие уязвимостей SQL-инъекции

Необходимо просматривать все фрагменты кода, вызывающие инструкции EXECUTE, EXECили sp_executesql. Чтобы выявить процедуры, содержащие эти инструкции, можно использовать запросы, подобные следующему. Этот запрос проверяет наличие 1, 2, 3 или 4 пробелов после слов EXECUTE и EXEC.

SELECT object_Name(id)
FROM syscomments
WHERE UPPER(TEXT) LIKE '%EXECUTE (%'
    OR UPPER(TEXT) LIKE '%EXECUTE  (%'
    OR UPPER(TEXT) LIKE '%EXECUTE   (%'
    OR UPPER(TEXT) LIKE '%EXECUTE    (%'
    OR UPPER(TEXT) LIKE '%EXEC (%'
    OR UPPER(TEXT) LIKE '%EXEC  (%'
    OR UPPER(TEXT) LIKE '%EXEC   (%'
    OR UPPER(TEXT) LIKE '%EXEC    (%'
    OR UPPER(TEXT) LIKE '%SP_EXECUTESQL%';

Заключение параметров с помощью QUOTENAME() и REPLACE()

Убедитесь, что в каждой выбранной хранимой процедуре все используемые в динамическом Transact-SQL переменные обрабатываются правильно. Данные, поступающие из входных параметров хранимой процедуры или считываемые из таблицы, должны быть заключены в QUOTENAME() или REPLACE(). Помните, что значение @variable, которое передаётся в QUOTENAME(), имеет тип sysname и максимальную длину 128 символов.

@variable Рекомендуемая оболочка
Имя защищаемого объекта QUOTENAME(@variable)
Строка из <128 символов QUOTENAME(@variable, '''')
> Строка из 128 символов REPLACE(@variable,'''', '''''')

При использовании этого приёма инструкцию SET можно изменить следующим образом:

-- Before:
SET @temp = N'SELECT * FROM authors WHERE au_lname ='''
    + @au_lname + N'''';

-- After:
SET @temp = N'SELECT * FROM authors WHERE au_lname = '''
    + REPLACE(@au_lname, '''', '''''') + N'''';

Инъекция, вызванная усечением данных

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

В этом примере у нас есть буфер с максимальной @command длиной 200 символов. Нам потребуется в общей сложности 154 символа, чтобы задать пароль для 'sa': 26 для выражения UPDATE, 16 для условия WHERE, 4 для 'sa', и 2 для кавычек вокруг QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Но, так как @new она объявлена как sysname, эта переменная может содержать только 128 символов. Мы можем обойти эту проблему, передав несколько символов одинарной кавычки в @new.

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
    + ' WHERE username=' + QUOTENAME(@loginname, '''')
    + ' AND password=' + QUOTENAME(@old, '''')

-- Execute the command.
EXEC (@command);
GO

Если злоумышленник передает 154 символов в буфер с 128 символами, он может задать новый пароль sa , не зная старый пароль.

EXEC sp_MySetPassword 'sa',
    'dummy',
    '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

По этой причине следует использовать большой буфер для переменной команды или напрямую выполнить динамический Transact-SQL внутри инструкции EXECUTE .

Усечение при использовании QUOTENAME(@variable, '''') и REPLACE()

Строки, возвращаемые QUOTENAME() и REPLACE(), усекаются без предупреждения, если их длина превышает выделенное для них пространство. Хранимая процедура, созданная в следующем примере, показывает, что может произойти.

В этом примере данные, хранящиеся во временных переменных, усечены, так как размер @login@oldpasswordбуфера и @newpassword составляет только 128 символов, но QUOTENAME() может возвращать до 258 символов. Если @new содержит 128 символов, то @newpassword может иметь значение 123... n, где n — 127-й символ. Поскольку строка, возвращаемая QUOTENAME(), усечена, она может выглядеть как следующее выражение:

UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
    + ' WHERE username = ' + @login
    + ' AND password = ' + @oldpassword;

-- Execute the command.
EXEC (@command);
GO

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

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

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

В этом примере данные усечены, так как буферы, выделенные для @login, @oldpassword и @newpassword могут содержать только 128 символов, но QUOTENAME() могут возвращать до 258 символов. Если @new содержит 128 символов, @newpassword может быть '123...n', где n находится 127-й символ. Поскольку строка, возвращаемая QUOTENAME(), усечена, ей можно придать вид следующего выражения:

UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password = '''
    + @newpassword + ''' WHERE username = '''
    + @login + ''' AND password = ''' + @oldpassword + '''';

-- Execute the command.
EXEC (@command);
GO

Как и в случае с QUOTENAME(), усечения строк с помощью REPLACE() можно избежать, объявляя временные переменные достаточно большого размера для всех случаев. По возможности следует вызывать QUOTENAME() или REPLACE() непосредственно внутри динамического Transact-SQL. Или же необходимый размер буфера можно рассчитать следующим образом. Для @outbuffer = QUOTENAME(@input) размер @outbuffer должен быть равен 2 * (len(@input) + 1). При использовании функции REPLACE() и двойных кавычек, как в предыдущем примере, достаточно буфера размером 2 * len(@input) .

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

WHILE LEN(@find_string) > 0, required buffer size =
    ROUND(LEN(@input) / LEN(@find_string), 0)
        * LEN(@new_string) + (LEN(@input) % LEN(@find_string))

Усечение при применении QUOTENAME(@variable, ']')

Усечение может происходить, когда имя защищаемого объекта ядра СУБД передается операторам, использующим форму QUOTENAME(@variable, ']'). Этот сценарий показан в следующем примере.

В этом примере @objectname должен предусматривать 2 * 258 + 1 символ.

CREATE PROCEDURE sp_MyProc
    @schemaname SYSNAME,
    @tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;

SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
    -- Do some operations.
GO

При объединениях значений sysname следует использовать временные переменные, достаточно большие, чтобы содержать не более 128 символов на значение. По возможности вызовите QUOTENAME() непосредственно внутри динамического Transact-SQL. Или же необходимый размер буфера можно рассчитать, как это показано в предыдущем разделе.