Injeção de SQL

Aplica-se a: SQL Server Banco de Dados SQL do Azure Instância Gerenciada de SQL do Azure Azure Synapse Analytics Analytics Platform System (PDW)

A injeção de SQL é um ataque em que o código mal-intencionado é inserido em cadeias de caracteres, passadas posteriormente para uma instância do mecanismo de banco de dados do SQL Server para análise e execução. Qualquer procedimento que construa instruções SQL deve ser verificado quanto a vulnerabilidades de injeção porque o mecanismo de banco de dados executa todas as consultas sintaticamente válidas que receber. Mesmo dados com parâmetros podem ser manipulados por um invasor qualificado e determinado.

Como funciona a injeção de SQL

A forma principal de injeção SQL consiste em inserção direta de código em variáveis de entrada de usuário, concatenadas com comandos SQL e executadas. Um ataque menos direto injeta código mal-intencionado em cadeias de caracteres destinadas a armazenamento em uma tabela ou como metadados. Quando as cadeias de caracteres armazenadas são concatenadas dem seguida em um comando SQL dinâmico, o código mal-intencionado é executado.

O processo de injeção funciona encerrando prematuramente uma cadeia de caracteres de texto e anexando um novo comando. Como o comando inserido pode ter sequências extras anexadas a ele antes de ser executado, o malfeitor encerra a cadeia de caracteres injetada com uma marca de comentário --. O texto subsequente é ignorado no momento da execução.

O script a seguir mostra uma injeção SQL simples. O script cria uma consulta SQL concatenando cadeias de caracteres codificadas com uma cadeia de caracteres inserida pelo usuário:

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

O usuário é solicitado a inserir o nome de uma cidade. Se ele inserir Redmond, a consulta criada pelo script será semelhante ao seguinte exemplo:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

No entanto, suponha que o usuário insira o seguinte texto:

Redmond';drop table OrdersTable--

Nesse caso, o script gera a seguinte consulta:

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

O ponto-e-vírgula (;) denota o término de uma consulta e o início de outra. O hífen duplo (--) indica que o restante da linha atual é um comentário e deve ser ignorado. Se o código modificado estiver sintaticamente correto, será executado pelo servidor. Quando o mecanismo de banco de dados processar essa instrução, ele seleciona primeiro todos os registros em OrdersTable onde ShipCity é Redmond. Em seguida, o mecanismo de banco de dados descarta OrdersTable.

Contanto que o código SQL injetado esteja sintaticamente correto, a violação não poderá ser detectada programaticamente. Portanto, é necessário validar todas as entradas de usuário e verificar com cuidado o código que executa comandos SQL construídos no servidor que você usa. Práticas recomendadas de codificação são descritas nas seções seguintes deste artigo.

Valide todas as entradas

Sempre valide entrada de usuário testando tipo, comprimento, formato e intervalo. Quando você estiver implementando precauções contra entrada mal-intencionada, considere os cenários de arquitetura e implantação de seu aplicativo. Lembre-se de que programas projetados para executar em um ambiente seguro podem ser copiados para um ambiente não seguro. As sugestões seguintes devem ser consideradas práticas recomendadas:

  • Não faça suposições sobre o tamanho, o tipo ou o conteúdo dos dados recebidos pelo seu aplicativo. Por exemplo, você deve fazer a seguinte avaliação:

    • Como seu aplicativo se comporta se um usuário errôneo ou mal-intencionado inserir um arquivo de vídeo de 2 GB onde seu aplicativo espera um código postal?

    • Como seu aplicativo se comporta se uma instrução DROP TABLE for inserida em um campo de texto?

  • Teste o tamanho e tipo de dados de entrada e imponha limites apropriados. Isso pode ajudar a impedir excesso deliberado de buffer.

  • Teste o conteúdo de variáveis de cadeia de caracteres e só aceite valores esperados. Rejeite entradas que contenham dados binários, sequências de escape e caracteres de comentário. Isso pode ajudar a impedir injeção de script e proteger contra explorações de excesso de buffer.

  • Quando você estiver trabalhando com documentos XML, valide todos os dados em seu esquema à medida que são inseridos.

  • Nunca construa instruções Transact-SQL diretamente da entrada do usuário.

  • Use procedimentos armazenados para validar entrada de usuário.

  • Em ambientes de várias camadas, todos os dados devem ser validados antes de serem admitidos à zona de confiança. Os dados que não passam pelo processo de validação devem ser rejeitados e um erro deve ser retornado à camada anterior.

  • Implemente várias camadas de validação. Precauções tomadas contra usuários casualmente mal-intencionados podem ser ineficazes contra determinados invasores. Uma prática mais recomendada é validar a entrada na interface do usuário e em todos os pontos subsequentes onde ele cruza um limite confiável.

    Por exemplo, a validação de dados em um aplicativo cliente pode evitar injeção de script simples. No entanto, se a próxima camada assumir que sua entrada já foi validada, qualquer usuário mal-intencionado que possa contornar um cliente poderá ter acesso irrestrito ao sistema.

  • Nunca concatene entrada de usuário que não seja validada. A concatenação de cadeia de caracteres é o ponto principal de entrada de injeção de script.

  • Não aceite as seguintes cadeias de caracteres nos campos dos quais os nomes de arquivo podem ser criados: AUX, CLOCK$, COM1 até COM8, CON, CONFIG$, LPT1 até LPT8, NUL e PRN.

Sempre que puder, rejeite entrada que contenha os caracteres a seguir.

Caractere de entrada Significado em Transact-SQL
; Delimitador de consulta.
' Delimitador de cadeia de dados de caractere.
-- Delimitador de comentário de linha única. O texto após -- até o final dessa linha não é avaliado pelo servidor.
/*** ... ***/ Delimitadores de comentário. Texto entre /* e */ não é avaliado pelo servidor.
xp_ Usado no início do nome de procedimentos armazenados estendidos de catálogo, como xp_cmdshell.

Use parâmetros SQL de tipo seguro

A coleção Parameters no mecanismo de banco de dados fornece verificação de tipo e validação de tamanho. Se você usar a coleção Parameters, a entrada será tratada como um valor literal e não como código executável. Outro benefício de usar a coleção Parameters é que você pode impor verificações de tipo e tamanho. Valores fora do intervalo disparam uma exceção. O seguinte fragmento de código mostra o uso da coleção 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;

Nesse exemplo, o parâmetro @au_id é tratado como um valor literal e não como código executável. Esse valor é verificado quanto ao tipo e comprimento. Se o valor de @au_id não estiver de acordo com o tipo especificado e as restrições de comprimento, uma exceção é gerada.

Use entrada com parâmetros com procedimentos armazenados

Os procedimentos armazenados poderão ser suscetíveis a injeção SQL se usarem entrada não filtrada. Por exemplo, o código seguinte é vulnerável:

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

Se você usar procedimentos armazenados, deverá usar parâmetros como entrada.

Use coleção Parâmetros com SQL dinâmico

Se você não puder usar procedimentos armazenados, ainda poderá usar parâmetros, como mostrado no exemplo de código a seguir.

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;

Entrada de filtro

A filtragem de entrada também pode ser útil para proteger contra injeção de SQL, removendo caracteres de escape. Porém, por causa do número grande de caracteres que podem causar problemas, a filtragem não é uma defesa confiável. O exemplo a seguir procura o delimitador de cadeia de caracteres.

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

Cláusulas LIKE

Se você usar uma cláusula LIKE, os caracteres curinga ainda deverão ter escape:

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

Revisar o código para injeção SQL

É necessário examinar todo o código que chama EXECUTE, EXECou sp_executesql. Você pode usar consultas semelhantes à que segue para ajudá-lo a identificar procedimentos que contenham essas instruções. Essa consulta verifica se existem 1, 2, 3 ou 4 espaços após as palavras EXECUTE ou 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%';

Envolver parâmetros com QUOTENAME () e REPLACE()

Em cada procedimento armazenado selecionado, verifique se todas as variáveis usadas no Transact-SQ dinâmico estão sendo tratadas corretamente. Dados oriundos de parâmetros de entrada de procedimento armazenado ou que são lidos de uma tabela devem ser envolvidos em QUOTENAME() ou REPLACE(). Lembre-se de que o valor da @variable que é passado para QUOTENAME() é de sysname e tem um comprimento máximo de 128 caracteres.

@variable Invólucro recomendado
Nome de um protegível QUOTENAME(@variable)
Cadeia de caracteres de < = 128 caracteres QUOTENAME(@variable, '''')
Cadeia de caracteres de > 128 caracteres REPLACE(@variable,'''', '''''')

Quando você usa essa técnica, uma instrução SET pode ser examinada da seguinte forma:

-- 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'''';

Injeção habilitada por truncamento de dados

Qualquer Transact-SQL dinâmico atribuído a uma variável é truncado se for maior do que o buffer alocado para aquela variável. Um invasor que é capaz de impor truncamento de instrução ao passar cadeias de caracteres longas inesperadamente para um procedimento armazenado pode manipular o resultado. Por exemplo, o seguinte exemplo de procedimento armazenado é vulnerável a injeção habilitada por truncamento.

Neste exemplo, temos um buffer @command com um comprimento máximo de 200 caracteres. Precisamos de um total de 154 caracteres para definir a senha de 'sa': 26 para a instrução UPDATE, 16 para a cláusula WHERE, 4 para 'sa', e 2 para aspas cercadas por QUOTENAME(@loginname): 200 - 26 - 16 - 4 - 2 = 154. Mas, como @new é declarada como sysname, essa variável só pode conter 128 caracteres. Podemos resolver isso passando algumas aspas simples em @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

Se um invasor passar 154 caracteres para um buffer de 128 caracteres, ele pode definir uma nova senha para sa sem conhecer a senha antiga.

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

Por esse motivo, é necessário usar um buffer grande para uma variável de comando ou executar diretamente o Transact-SQL dinâmico na instrução EXECUTE.

Truncamento quando QUOTENAME (@ variável, '''') e REPLACE() são usados

Cadeias de caracteres retornadas por QUOTENAME() e REPLACE() são silenciosamente truncadas se ultrapassarem o espaço alocado. O procedimento armazenado criado nos exemplos a seguir mostram o que pode acontecer.

Neste exemplo, os dados armazenados em variáveis temporárias são truncados, porque o tamanho do buffer de @login, @oldpassword e @newpassword é de apenas 128 caracteres, mas QUOTENAME() pode retornar até 258 caracteres. Se @new contém 128 caracteres, então @newpassword pode ser 123... n, onde n é o 127º caractere. Como a cadeia de caracteres retornada por QUOTENAME() é truncada, ela pode ser feita para se parecer com a seguinte instrução:

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

Portanto, a instrução a seguir define as senhas de todos os usuários como o valor passado no código anterior.

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

Você pode impor truncamento de cadeia de caracteres excedendo o espaço do buffer alocado ao usar REPLACE(). O procedimento armazenado criado nos exemplos a seguir mostram o que pode acontecer.

Neste exemplo, os dados são truncados porque os buffers alocados para @login, @oldpassword e @newpassword podem conter apenas 128 caracteres, mas QUOTENAME() pode retornar até 258 caracteres. Se @new contém 128 caracteres, @newpassword pode ser '123...n', onde n é o 127º caractere. Como a cadeia de caracteres retornada por QUOTENAME() é truncada, ela pode ser feita para se parecer com a seguinte instrução:

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

Assim como acontece com QUOTENAME(), o truncamento da cadeia de caracteres por REPLACE() pode ser evitado declarando variáveis temporárias suficientemente grandes para todos os casos. Quando possível, você deve chamar QUOTENAME() ou REPLACE() diretamente dentro do Transact-SQL dinâmico. Caso contrário, você pode calcular o tamanho do buffer exigido como segue. Para @outbuffer = QUOTENAME(@input), o tamanho de @outbuffer deve ser 2 * (len(@input) + 1). Quando você usa REPLACE() e aspas duplas, como no exemplo anterior, um buffer de 2 * len(@input) é suficiente.

O cálculo seguinte cobre todos os casos:

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

Truncamento quando QUOTENAME(@variable, ']') é usado

Pode ocorrer truncamento quando o nome de um protegível do mecanismo de banco de dados é passado a instruções que usam a forma QUOTENAME(@variable, ']'). O exemplo a seguir demonstra esse cenário.

Neste exemplo, @objectname deve permitir 2 * 258 + 1 caracteres.

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

Quando você concatena valores do tipo sysname, deve usar variáveis temporárias suficientemente grandes para manter no máximo 128 caracteres por valor. Se possível, chame QUOTENAME() diretamente no Transact-SQL dinâmico. Caso contrário, você pode calcular o tamanho do buffer exigido como explicado na seção anterior.