Атака путем внедрения кода SQL

Применимо: SQL Server База данных SQL Azure Управляемый экземпляр SQL Azure azure Synapse Analytics Analytics Platform System (PDW)

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

Как работает внедрение SQL

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

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

Следующий скрипт показывает простую атаку SQL Injection. Скрипт создает 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 через , CONCONFIG$LPT1 , через , через LPT8COM8, и . NULPRN

По возможности отклоняйте вводимые данные, содержащие следующие символы:

Входной символ Значение в языке 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);

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

Использование коллекции параметров с динамическим 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. Или же необходимый размер буфера можно рассчитать, как это показано в предыдущем разделе.