msdn12_capa.JPG

Clique aqui para ler todos os artigos desta edição

 

SEGURANÇA DE DADOS
Acabe com os ataques de injeção de SQL antes que eles acabem com você

por Paul Litwin

Este artigo discute

Este artigo usa as seguintes tecnologias:

·         Como trabalhar com ataques de injeção de SQL

·         Testes e vulnerabilidades

·         Validação de entrada de usuário

·         Usando recursos do .NET para previnir ataques

·         Importância de tratar Exceptions

ASP.NET, C#, SQL

 

Download:

SQLInjection.exe (153KB)

Chapéu

ASP.NET

 

 

Armado com tecnologias avançadas de servidor como ASP.NET e poderosos servidores de bancos de dados como o Microsoft® SQL Server¢, os desenvolvedores são capazes de criar Web sites dinâmicos comandados por dados com incrível facilidade. Mas o poder do ASP.NET e do SQL podem facilmente ser usados contra você pelos hackers, montando uma classe muito comum de ataque: a injeção de SQL.

A idéia básica por trás de um ataque por injeção de SQL é a seguinte: você cria uma página Web que permite que o usuário entre com texto em uma caixa de texto que será usada para executar uma consulta no banco de dados. O hacker insere uma instrução SQL malformada no textbox que altera a natureza da consulta, de modo que será usada para invadir, alterar ou danificar o banco de dados remoto. Como isso é possível? Vou ilustrar com um exemplo.

 

Bom SQL que se torna ruim

Mutos aplicativos do ASP.NET usam um formulário como o mostrado na Listagem 1 para autenticar os usuários. Quando o usuário clicar no botão Login de BadLogin.aspx, o método cmdLogin_Click tentará autenticar o usuário executando uma consulta que contará o número de registros presentes na tabela Users, onde UserName e Password corresponderão aos valores que o usuário tiver inserido nos controles textbox do formulário.

 

Listagem 1 BadLogin.aspx.cs

private void cmdLogin_Click(object sender, System.EventArgs e) {

  string strCnx =

     "server=localhost;database=northwind;uid=sa;pwd=;";

  SqlConnection cnx = new SqlConnection(strCnx);

 

  cnx.Open();

 

  //Este código é suscetível a ataques de injeção SQL

  string strQry = "SELECT Count(*) FROM Users WHERE UserName='" +

  txtUser.Text + "' AND Password='" +

  txtPassword.Text + "'";

 int intRecs;

 

  SqlCommand cmd = new SqlCommand(strQry, cnx);

  intRecs = (int) cmd.ExecuteScalar();

 

  if (intRecs>0) {

        FormsAuthentication.RedirectFromLoginPage(txtUser.Text, false);

  }

  else {

    lblMsg.Text = "Login attempt failed.";

  }

  cnx.Close();

}

 

Na maioria dos casos, o formulário funciona exatamente como o esperado. Um usuário insere um nome e uma senha que correspondam a um registro na tabela Users. É usada uma consulta SQL gerada dinamicamente para ler o número de linhas correspondentes. O usuário é então autenticado e redirecionado à página requisitada. Os usuários que não inserirem nome e/ou senha válidos não serão autenticados. Contudo, aqui também é possível que um hacker insira o seguinte texto aparentemente inofensivo na textbox UserName para conseguir entrar no sistema sem ter que conhecer um nome e uma senha válidos:

 

' Or 1=1 --

 

O hacker invade o sistema injetando SQL malformado na consulta. Essa invasão específica funciona porque a consulta executada é formada pela concatenação de uma string fixa e valores inseridos pelo usuário, como mostrado a seguir:

 

string strQry = "SELECT Count(*) FROM Users WHERE UserName='" +

txtUser.Text + "' AND Password='" +

txtPassword.Text + "'";

 

No caso de o usuário inserir um nome de usuário válido igual a "Paul" e uma senha igual a "password", o strQry será:

 

SELECT Count(*) FROM Users WHERE UserName='Paul' AND Password='password'

 

Mas se o hacker inserir

 

Or 1=1 --

 

a consulta agora será:

 

SELECT Count(*) FROM Users WHERE UserName='' Or 1=1 --' AND Password=''

 

Como um par de hífens indica o início de um comentário no SQL, a consulta passa a ser somente:

 

SELECT Count(*) FROM Users WHERE UserName='' Or 1=1

 

A expressão 1=1 é sempre verdadeira para todas as linhas da tabela e uma expressão verdadeira unida por um OR a outra, com outra expressão, retornará sempre verdadeiro. Então, supondo que há pelo menos uma linha da tabela Users, esse SQL sempre retornará uma contagem de registros diferente de zero.

Nem todos os ataques por injeção de SQL envolvem a autenticação de formulários. Tudo o que é preciso é um aplicativo com algum SQL construído dinamicamente e uma inserção de dados por um usuário não confiável. Dadas as condições corretas, a extensão dos danos causados por esse tipo de ataque pode ser limitada somente pela extensão do conhecimento do hacker sobre a linguagem SQL e a configuração do banco de dados.

Agora analise o código mostrado na Listagem 2, tirado de BadProductList.aspx. Esta página exibe produtos do banco de dados Northwind e permite que os usuários filtrem a lista resultante de produtos usando um textbox chamado txtFilter. Como no último exemplo, a página está pronta para ataques de injeção de SQL porque o SQL executado é construído dinamicamente a partir de um valor inserido pelo usuário. Esta página em particular é o paraíso dos hackers porque pode ser seqüestrada pelo hacker astuto para revelar informações secretas, alterar dados no banco de dados, danificar os controles do banco de dados e até mesmo criar novas contas de usuário do banco de dados.

 

Listagem 2 BadProductList.aspx.cs

 

private void cmdFilter_Click(object sender, System.EventArgs e) {

  dgrProducts.CurrentPageIndex = 0;

  bindDataGrid();

}

 

private void bindDataGrid() {

  dgrProducts.DataSource = createDataView();

  dgrProducts.DataBind();

}

 

private DataView createDataView()  {

  string strCnx =

     "server=localhost;uid=sa;pwd=;database=northwind;";

  string strSQL = "SELECT ProductId, ProductName, " +

  "QuantityPerUnit, UnitPrice FROM Products";

 

  //Este código é suscetível a ataques de injeção SQL

  if (txtFilter.Text.Length > 0) {

      strSQL += " WHERE ProductName LIKE '" +

      txtFilter.Text + "'";

  }

 

  SqlConnection cnx  = new SqlConnection(strCnx);

  SqlDataAdapter sda = new SqlDataAdapter(strSQL, cnx);

  DataTable dtProducts = new DataTable();

 

  sda.Fill(dtProducts);

 

  return dtProducts.DefaultView;

}

 

A maioria dos bancos de dados compatíveis com SQL, inclusive o SQL Server, armazena metadados em uma série de tabelas de sistema com os nomes sysobjects, syscolumns, sysindexes, e assim por diante. Isto significa que um hacker poderia usar as tabelas do sistema para conseguir informações do esquema de um banco de dados, para ajudar no posterior comprometimento do banco de dados. Por exemplo, o texto inserido na textbox txtFilter poderia ser usado para revelar os nomes das tabelas de usuários do banco de dados:

 

' UNION SELECT id, name, '', 0 FROM sysobjects WHERE xtype ='U' --

 

A instrução UNION, especificamente é útil para um hacker porque permite que ele reúna os resultados da consulta. Neste caso, o hacker uniu os nomes das tabelas de usuário do banco de dados à consulta original da tabela Products. O único truque é combinar o número e os datatypes das colunas com os da consulta original. A consulta anterior poderia revelar que há uma tabela chamada Users no banco de dados. Uma Segunda consulta poderia revelar as colunas da tabela Users. Com essas informações, o hacker poderia inserir o seguinte na textbox txtFilter:

 

' UNION SELECT 0, UserName, Password, 0 FROM Users --

 

A inserção desta consulta revela os nomes e as senhas dos usuários constantes na tabela Users, como mostrado na Figura 1.

 

image001.gif

Figura 1 Consulta à tabela Users

 

Os ataques por injeção de SQL também podem ser usados para alterar dados ou danificar o banco de dados. O hacker da injeção de SQL pode inserir no textbox txtFilter para alterar o preço do primeiro produto de $18 para $0.01 e depois comprar rapidamente algumas caixas do produto antes que alguém note o que aconteceu:

 

'; UPDATE Products SET UnitPrice = 0.01 WHERE ProductId = 1--

 

Essa alteração funciona porque o SQL Server permite que você reúna numa string várias instruções SQL separadas por um ponto-e-vírgula ou um espaço. Neste exemplo, o DataGrid não apresenta coisa alguma, mas a consulta de atualização é executada com sucesso. Essa mesma técnica pode ser usada para executar uma instrução DROP TABLE ou para executar uma stored procedure de sistema que criasse uma nova conta de usuário e acrescentasse aquele usuário ao papel de sysadmin. Todas essas invasões são possíveis com a página BadProductList.aspx mostrada na Listagem 2.

 

Igualdade nas oportunidades de invasão

É importante entender que os ataques por injeção de SQL não se limitam ao SQL Server. Outros bancos de dados, incluindo Oracle, MySQL, DB2, Sybase e outros, são susceptíveis a esse tipo de ataque. Os ataques por injeção de SQL são possíveis porque a linguagem SQL contém diversos recursos que os tornam bastante poderosos e flexíveis. São eles:

      A possibilidade de incorporar comentários em uma instrução SQL usando um par de hífens

      A possibilidade de unir em uma string várias instruções SQL e de executá-las em lote

      A possibilidade de usar o SQL para consultar metadados a partir de um conjunto padrão de tabelas de sistema

 

Geralmente, quanto mais poderoso o dialeto do SQL suportado pelo banco de dados, mais susceptível a ataques será esse banco de dados. Assim, não é de surpreender que o SQL Server seja um alvo tão conhecido dos ataques por injeção.

Os ataques por injeção de SQL não se limitam aos aplicativos do ASP.NET. Aplicativos comuns em ASP, Java, JSP e PHP também correm o mesmo risco. Na verdade, os ataques de injeção de SQL podem ser efetuados também contra aplicativos no computador local. Por exemplo, incluí nos arquivos para download deste arquivo um exemplo de aplicativo do Windows® Forms chamado SQLInjectWinForm, que também é susceptível aos ataques de injeção de SQL.

Embora seja fácil apontar uma ou outra medida básica para evitar os ataques de injeção de SQL, é melhor atacar o problema adotando um método em níveis. Assim, se uma de suas medidas for contornada devido a alguma vulnerabilidade, você ainda estará protegido. Os níveis recomendados estão resumidos na Tabela 1.

 

Tabela 1 Evitando ataques de injeção de SQL

Princípio

Implementação

Nunca confiar nas informações do usuário

Valide todas as entradas de textbox através de controles de validação, expressões regulares, código, e assim por diante

Nunca use SQL dinâmico

Use SQL parametrizado ou procedimentos armazenados

Nunca conecte a um banco de dados com uma conta de nível de administrador

Use uma conta de acesso limitado para conectar-se ao banco de dados

Não armazene segredos em texto puro

Criptografe ou codifique por hash senhas e outros dados secretos; você deve criptografar também as strings de conexão

As exceções devem divulgar o mínimo de informações

Não revele informações demais nas mensagens de erro; use customErrors para exibir informações mínimas no caso de um erro não tratado; defina o valor de debug com false

 

Toda inserção é maligna

O primeiro princípio relacionado na Tabela 1 é extremamente importante: parta do princípio de que toda inserção de dados é maligna! Nunca se deve usar inserções de dados do usuário em uma consulta ao banco de dados. Os controles de validação do ASP.NET, especialmente o controle RegularExpressionValidator, são boas ferramentas de validação de inserção de dados.

Há dois métodos básicos de validação: não permitir a inserção de caracteres problemáticos ou permitir somente um pequeno número de caracteres necessários. Embora você possa facilmente negar permissão  a alguns caracteres problemáticos, tais como o hífen e p apóstrofo, esse método é pouco eficaz por dois motivos: primeiro, você pode esquecer algum caractere que poderia ser útil aos hackers e, segundo, geralmente há mais de uma maneira de representar um caractere perigoso. Por exemplo, um hacker pode ser capaz de escapar um apóstrofo de modo que seu código de validação não o pegue e passe ao banco de dados, que o tratará como um caractere de apóstrofo. Um método melhor é identificar os caracteres permitidos e permitir somente esses caracteres. Este método exige mais trabalho, mas garante um controle muito mais forte sobre as inserções e é mais seguro. Independente do método adotado, também será conveniente limitar o comprimento da inserção, porque algumas invasões exigem um número grande de caracteres.

O arquivo GoodLogin.aspx (disponível para download) contém dois controles de validação de expressões, uma para o nome do usuário e outra para a senha, com o seguinte valor de ValidationExpression que limita entradas a um número de dígitos entre 4 e 12 caracteres, alfabéticos, e o sublinhado:

 

[\d_a-zA-Z]{4,12}

 

Talvez você tenha que permitir que o usuário insira caracteres potencialmente perigosos em um textbox. Por exemplo, os usuários podem inserir um apóstrofo como parte do nome de uma pessoa. Nesses casos, você pode tornar esse apóstrofo inofensivo usando uma expressão regular ou o método String.Replace para substituir cada instância do apóstrofo por aspas. Por exemplo:

 

string strSanitizedInput = strInput.Replace("'", "''");

 

Evite SQL dinâmico

Todos os ataques por injeção de SQL que demonstrei neste artigo dependem da execução de instruções SQL dinâmicas, construídas pela concatenação de SQL com valores inseridos pelo usuário. O emprego de SQL parametrizado, no entanto, reduz bastante a capacidade do hacker de injetar SQL no código.

O código apresentado na Listagem 3 emprega SQL parametrizado para impedir os ataques por injeção. SQL parametrizado é excelente se você absolutamente precisar usar SQL ad hoc. Isso poderá ser necessário se o seu departamento de TI não acreditar em stored procedures ou usar um produto como o MySQL, que não os suportava até a versão 5.0. Se for possível, no entanto, você deverá usar stored procedures para ter o recurso adicional de remover todas as permissões das tabelas básicas no banco de dados e, assim, remover a capacidade de criar consultas como as mostradas na Figura 1. O arquivo BetterLogin.aspx, mostrado na Listagem 4, usa uma stored procedure, procVerifyUser, para validar usuários.

 

Listagem 3 GoodLogin.aspx.cs

private void cmdLogin_Click(object sender, System.EventArgs e) {

  string strCnx = ConfigurationSettings.AppSettings["cnxNWindBad"];

  using (SqlConnection cnx = new SqlConnection(strCnx))

  {

    SqlParameter prm;

 

    cnx.Open();

 

    string strQry =

    "SELECT Count(*) FROM Users WHERE UserName=@username " +

    "AND Password=@password";

    int intRecs;

 

    SqlCommand cmd = new SqlCommand(strQry, cnx);

    cmd.CommandType= CommandType.Text;

 

    prm = new SqlParameter("@username",SqlDbType.VarChar,50);

    prm.Direction=ParameterDirection.Input;

    prm.Value = txtUser.Text;

    cmd.Parameters.Add(prm);

 

    prm = new SqlParameter("@password",SqlDbType.VarChar,50);

    prm.Direction=ParameterDirection.Input;

    prm.Value = txtPassword.Text;

    cmd.Parameters.Add(prm);           

 

    intRecs = (int) cmd.ExecuteScalar();

 

    if (intRecs>0) {

            FormsAuthentication.RedirectFromLoginPage(txtUser.Text, false);

    }

    else {

      lblMsg.Text = "Login attempt failed.";

    }

  }

}

 

 

Listagem 4 BetterLogin.aspx.cs

private void cmdLogin_Click(object sender, System.EventArgs e) {

  string strCnx =

        ConfigurationSettings.AppSettings["cnxNWindBetter"];

  using (SqlConnection cnx = new SqlConnection(strCnx))

  {

    SqlParameter prm;

 

    cnx.Open();

 

    string strAccessLevel;

 

    SqlCommand cmd = new SqlCommand("procVerifyUser", cnx);

    cmd.CommandType= CommandType.StoredProcedure;

 

    prm = new SqlParameter("@username",SqlDbType.VarChar,50);

    prm.Direction=ParameterDirection.Input;

    prm.Value = txtUser.Text;

    cmd.Parameters.Add(prm);

 

    prm = new SqlParameter("@password",SqlDbType.VarChar,50);

    prm.Direction=ParameterDirection.Input;

    prm.Value = txtPassword.Text;

    cmd.Parameters.Add(prm);           

 

    strAccessLevel = (string) cmd.ExecuteScalar();

 

    if (strAccessLevel.Length>0) {

            FormsAuthentication.RedirectFromLoginPage(txtUser.Text, false);

    }

    else {

      lblMsg.Text = "Login attempt failed.";

    }

  }

}

 

Execute com o mínimo de privilégios

Uma das práticas ruins demonstradas em BadLogin.aspx e BadProductList.aspx é o uso se uma string de conexão que usa a conta sa. Esta é a string de conexão, que se encontra no Web.config: