Partilhar via


Injeção de SQL

Aplica-se a:Banco de Dados SQL dodo AzureInstância Gerenciada SQL do Azuredo Azure Synapse Analyticsdo Analytics Platform System (PDW)Banco de Dados SQL no Microsoft Fabric

A injeção de SQL é um ataque no qual um código mal-intencionado é inserido em cadeias de caracteres que são posteriormente passadas 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 revisado para vulnerabilidades de injeção, porque o Mecanismo de Banco de Dados executa todas as consultas sintaticamente válidas que recebe. Até mesmo dados parametrizados podem ser manipulados por um atacante habilidoso e determinado.

Como funciona a injeção de SQL

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

O processo de injeção funciona encerrando prematuramente uma cadeia de texto e anexando um novo comando. Como o comando inserido pode ter cadeias de caracteres 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 de SQL simples. O script cria uma consulta SQL concatenando cadeias de caracteres codificadas juntamente 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 eles entrarem Redmond, a consulta montada pelo script será semelhante ao exemplo a seguir:

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

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

Redmond';drop table OrdersTable--

Nesse caso, o script monta a seguinte consulta:

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

O ponto-e-vírgula (;) indica o fim de uma consulta e o início de outra. O hífen duplo (--) indica que o resto da linha atual é um comentário e deve ser ignorado. Se o código modificado estiver sintaticamente correto, ele será executado pelo servidor. Quando o Mecanismo de Banco de Dados processa essa instrução, ele primeiro seleciona todos os registros em OrdersTable onde ShipCity está Redmond. Em seguida, o Mecanismo de Banco de Dados elimina OrdersTable.

Desde que o código SQL injetado esteja sintaticamente correto, a adulteração não pode ser detetada programaticamente. Portanto, você deve validar todas as entradas do usuário e revisar cuidadosamente o código que executa comandos SQL construídos no servidor que você usa. As práticas recomendadas de codificação são descritas nas seções a seguir neste artigo.

Validar todas as entradas

Sempre valide a entrada do usuário testando tipo, comprimento, formato e intervalo. Ao implementar precauções contra entradas mal-intencionadas, considere os cenários de arquitetura e implantação do seu aplicativo. Lembre-se de que os programas projetados para serem executados em um ambiente seguro podem ser copiados para um ambiente não seguro. As seguintes sugestões devem ser consideradas boas práticas:

  • Não faça suposições sobre o tamanho, tipo ou 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 errante 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 DROP TABLE instrução estiver incorporada em um campo de texto?

  • Teste o tamanho e o tipo de dados da entrada e imponha limites apropriados. Isso pode ajudar a evitar saturações deliberadas de buffer.

  • Teste o conteúdo das variáveis de cadeia de caracteres e aceite apenas os valores esperados. Rejeitar entradas que contenham dados binários, sequências de escape e caracteres de comentário. Isso pode ajudar a evitar a injeção de script e pode proteger contra algumas explorações de saturação de buffer.

  • Ao trabalhar com documentos XML, valide todos os dados em relação ao seu esquema à medida que são inseridos.

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

  • Use procedimentos armazenados para validar a entrada do usuário.

  • Em ambientes multicamadas, todos os dados devem ser validados antes da admissão na zona confiável. Os dados que não passam no processo de validação devem ser rejeitados e um erro deve ser retornado para a camada anterior.

  • Implemente várias camadas de validação. As precauções que você toma contra usuários casualmente mal-intencionados podem ser ineficazes contra determinados invasores. Uma prática melhor é validar a entrada na interface do usuário e em todos os pontos subsequentes em que ela cruza um limite de confiança.

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

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

  • Não aceite as seguintes cadeias de caracteres em campos a partir dos quais os nomes de ficheiro podem ser construídos: AUX, CLOCK$, do COM1 ao COM8, CON, CONFIG$, LPT1 ao LPT8, NUL e PRN.

Se possível, rejeite a entrada que contenha os seguintes caracteres.

Caractere de entrada Significado em Transact-SQL
; Delimitador de consultas.
' Delimitador de cadeia de caracteres de dados.
-- Delimitador de comentários de linha única. O texto que segue -- até o final dessa linha não é avaliado pelo servidor.
/*** ... ***/ Delimitadores de comentários. O texto entre /* e */ não é avaliado pelo servidor.
xp_ Usado no início do nome de procedimentos armazenados com extensão de catálogo, como xp_cmdshell.

Usar parâmetros SQL seguros para tipos

A Parameters coleção no Mecanismo de Banco de Dados fornece verificação de tipo e validação de comprimento. Se você usar a coleção, a Parameters entrada será tratada como um valor literal em vez de como código executável. Outro benefício de usar a Parameters coleção é que você pode impor verificações de tipo e comprimento. Valores fora do intervalo acionam uma exceção. O fragmento de código a seguir mostra como usar a Parameters coleção:

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;

Neste exemplo, o @au_id parâmetro é tratado como um valor literal em vez de como código executável. Este valor é verificado quanto ao tipo e comprimento. Caso o valor de @au_id não esteja em conformidade com as restrições de tipo e comprimento especificadas, uma exceção será lançada.

Usar entrada parametrizada com procedimentos armazenados

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

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

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

Utilizar a coleção de Parâmetros com SQL dinâmico

Se você não puder usar procedimentos armazenados, ainda poderá usar parâmetros, conforme 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 do filtro

A filtragem de entrada também pode ser útil na proteção contra injeção de SQL removendo caracteres de escape. No entanto, devido ao grande número de caracteres que podem representar 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 LIKE cláusula, os caracteres curinga ainda deverão ser escapados:

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

Código de revisão para injeção de SQL

Você deve revisar todo o código que chama EXECUTE, EXECou sp_executesql. Você pode usar consultas semelhantes às seguintes para ajudá-lo a identificar procedimentos que contenham essas instruções. Esta consulta verifica se há 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-SQL dinâmico são tratadas corretamente. Os dados provenientes dos parâmetros de entrada do procedimento armazenado ou que são lidos de uma tabela devem ser encapsulados em QUOTENAME() ou REPLACE(). Lembre-se de que o valor de @variable que é passado para QUOTENAME() é de sysname e tem um comprimento máximo de 128 caracteres.

@variable Invólucro recomendado
Nome de um elemento segurável QUOTENAME(@variable)
String de <= 128 caracteres QUOTENAME(@variable, '''')
Sequência de > 128 caracteres REPLACE(@variable,'''', '''''')

Quando se utiliza esta técnica, uma SET declaração pode ser revista 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 ativada por truncamento de dados

Qualquer Transact-SQL dinâmico atribuído a uma variável será truncado se for maior do que o buffer alocado para essa variável. Um atacante que consegue forçar o truncamento de declarações ao passar sequências de caracteres longas e inesperadas para um procedimento gravado pode manipular os resultados. Por exemplo, o procedimento armazenado de exemplo a seguir é vulnerável à injeção habilitada por truncamento.

Neste exemplo, temos um @command buffer com um comprimento máximo de 200 caracteres. Precisamos de um total de 154 caracteres para definir a senha de 'sa': 26 para a UPDATE declaração, 16 para a WHERE cláusula, 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 superar 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 saber a senha antiga.

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

Por esse motivo, você deve usar um buffer grande para uma variável de comando ou executar diretamente o Transact-SQL dinâmico dentro da EXECUTE instrução.

Truncamento quando QUOTENAME(@variable, '''') e REPLACE() são usados

As cadeias de caracteres retornadas por QUOTENAME() e REPLACE() são silenciosamente truncadas se excederem o espaço alocado. O procedimento armazenado criado no exemplo a seguir mostra o que pode acontecer.

Neste exemplo, os dados armazenados em variáveis temporárias são truncados, porque o tamanho do buffer de , @logine @oldpassword é de @newpasswordapenas 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, pode ser ajustada para parecer com a instrução seguinte.

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 para o valor que foi passado no código anterior.

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

Você pode forçar o truncamento de cadeia de caracteres excedendo o espaço do buffer alocado quando usar REPLACE(). O procedimento armazenado criado no exemplo a seguir mostra 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() podem 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, pode ser ajustada para parecer com a instrução seguinte.

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 no QUOTENAME(), o truncamento de sequência de caracteres por REPLACE() pode ser evitado ao declarar variáveis temporárias que sejam 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 necessário da seguinte maneira. Para @outbuffer = QUOTENAME(@input), o tamanho de @outbuffer deve ser 2 * (len(@input) + 1). Quando utilizas REPLACE() e aspas duplas, como no exemplo anterior, um buffer de 2 * len(@input) é suficiente.

O cálculo seguinte abrange 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

O truncamento pode ocorrer quando o nome de um Mecanismo de Banco de Dados protegível é passado para instruções que usam o formulário 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

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