Segurança do ASP.NET

Protegendo seus aplicativos ASP.NET contra hackers

Adam Tuliper

Quase todos os dias, os meios de comunicação noticiam que outro site foi invadido por hackers. Essas invasões constantes por grupos de hackers proeminentes fazem com que os desenvolvedores fiquem se perguntando se esses grupos estão usando técnicas avançadas para seu trabalho abominável. Embora alguns ataques modernos sejam bastante complexos, muitas vezes os que custam menos e são mais eficientes são notavelmente simples - e estão sendo aplicados há muitos anos. Por sorte, em geral é surpreendentemente fácil se proteger contra esses ataques.

Vou analisar alguns dos tipos mais comuns de ataques de hackers em uma série de dois artigos. No primeiro artigo, falarei sobre injeção de SQL e violação de parâmetros e, na segunda parte, que estará na edição de janeiro, abordarei scripts entre sites e falsificação de solicitação em diferentes locais.

Caso você esteja curioso para saber se apenas sites grandes têm que se preocupar com os hackers, a resposta é simples: todos os desenvolvedores devem se preocupar com tentativas de ataques de hackers contra seus aplicativos. É o seu trabalho proteger seus aplicativos; os usuários esperam isso de você. Até os aplicativos pequenos na Internet estão expostos a sondagens devido ao grande números de programas de invasão automática disponíveis. Vamos supor que a sua tabela de usuários ou clientes foi roubada e que as senhas dessas tabelas também são utilizadas para outros aplicativos. Certamente há uma recomendação para que os usuários sempre utilizem senhas diferentes, mas na prática eles não fazem isso. Você não quer ter que comunicar aos usuários que o aplicativo foi o caminho de entrada para o roubo de informações. O blog bastante modesto de um amigo, que relata sua viagem ao monte Everest, foi invadido por hackers e teve o conteúdo totalmente excluído, sem motivo aparente. Sem proteção, praticamente nenhum aplicativo está seguro.

A menos que a rede esteja fisicamente desconectada de qualquer dispositivo de comunicação externo, existe a possibilidade de ela ser invadida por alguém que se aproveite de problemas na configuração do proxy, por ataques de RDP ou à rede virtual privada (VPN), vulnerabilidades por execução de código remoto por um usuário interno com a simples visita a uma página da Web, senhas adivinhadas, regras de firewall inadequadas, Wi-Fi (a maior parte da segurança Wi-Fi pode ser decifrada por um invasor no seu estacionamento), truques de engenharia social que fazem com que as pessoas forneçam informações confidenciais, além de outros pontos de entrada. A menos que esteja completamente desconectado do mundo externo, nenhum ambiente pode ser considerado totalmente seguro.

Agora que chamei sua atenção (espero) para acreditar que a ameaça das invasões por hackers é muito real e que todos os seus aplicativos podem ser sondados, vamos conhecer essas invasões e saber como você pode se proteger contra elas!

Injeção de SQL

O que é isso? A injeção de SQL consiste em um ataque pelo qual um ou mais comandos são inseridos em uma consulta para compor uma nova consulta nunca pretendida pelo desenvolvedor. Quase sempre isso acontece quando é usado SQL dinâmico, ou seja, quando você está concatenando cadeias de caracteres no código para formar instruções SQL. A injeção de SQL pode ocorrer no código do Microsoft .NET Framework, se você está formando uma consulta ou uma chamada de procedimento, e também no código T-SQL no servidor, como no caso de SQL dinâmico em procedimentos armazenados.

A injeção de SQL é especialmente perigosa porque pode ser usada não somente para consultar e editar dados, mas também para executar comandos de banco de dados limitados apenas por permissões de conta de serviço de banco de dados ou de usuário de banco de dados. Se o seu SQL Server está configurado para executar como conta de administrador e o usuário do aplicativo pertence à função sysadmin, fique muito atento. Os ataques que usam injeção de SQL podem executar comandos de sistema para executar o seguinte:

  • Instalar backdoors
  • Transferir um banco de dados inteiro pela porta 80
  • Instalar sniffers (farejadores) de rede para roubar senhas e outros dados confidenciais
  • Decifrar senhas
  • Enumerar a rede interna, inclusive varrendo as portas de outras máquinas
  • Baixar arquivos
  • Executar programas
  • Excluir arquivos
  • Tornar-se parte de um botnet
  • Consultar senhas com preenchimento automático armazenadas no sistema
  • Criar novos usuários
  • Criar, excluir e editar dados; criar e descartar tabelas

Esta lista não é completa e o risco só é restrito pelas permissões em vigor — e a criatividade do hacker. 

A injeção de SQL já existe há tanto tempo que muitas vezes me perguntam se ela ainda é uma preocupação. A resposta é "certamente", e os hackers a usam com muita frequência. Na verdade, fora os ataques de negação de serviço (DoS), a injeção de SQL é o ataque mais comumente usado.

Como ela é explorada? Geralmente a injeção de SQL é explorada quando o usuário entra em uma página da Web ou por violação de parâmetros, que normalmente envolve alterar não apenas formulários ou URIs — cookies, cabeçalhos, entre outros, também estão sujeitos a ataques se o aplicativo usa esses valores em uma instrução SQL não segura (abordarei esse tópico mais adiante no artigo).

Vejamos um exemplo de injeção de SQL por violação de formulário. Este é o cenário que tenho visto muitas vezes em código de produção. O seu código pode não ser exatamente igual, mas essa é uma forma comum de os desenvolvedores verificarem credenciais de logon.

Esta é a instrução SQL formada dinamicamente para recuperar o logon do usuário:

string loginSql = string.Format("select * from users where loginid= '{0}

  ' and password= '{1} '"", txtLoginId.Text, txtPassword.Text);

Isto forma a instrução SQL:

    select * from dbo.users where loginid='Administrator' and  
    
        password='12345'

Por si só, a instrução não é um problema. Mas vamos supor que a entrada no campo do formulário seja semelhante à mostrada na Figura 1.

Malicious Input Instead of a Valid Username
Figura 1 Entrada mal-intencionada em vez de um nome de usuário válido

Esta entrada forma a instrução SQL:


    select * from dbo.users where loginid='anything' union select top 1 *
    
      from users --' and password='12345'

Esse exemplo introduz uma ID de logon "nada" desconhecida, que por si só não retornaria nenhum registro. No entanto, em seguida ela combina esses resultados com o primeiro registro do banco de dados, enquanto os próximos “--”inserem comentários em todo o restante da consulta, por isso é ignorada. O invasor não só consegue fazer logon, como também retorna um registro de usuário válido para o código de chamada sem na verdade conhecer um nome de usuário válido.

O seu código pode não replicar esse cenário exato, obviamente, mas o importante ao analisar os aplicativos é pensar na origem dos valores incluídos nas consultas, entre os quais estão os seguintes:

  • Campos de formulário
  • Parâmetros de URL
  • Valores armazenados no banco de dados
  • Cookies
  • Cabeçalhos
  • Arquivos
  • Armazenamento isolado

Talvez nem todos esses valores sejam claramente óbvios. Por exemplo, por que os cabeçalhos são um problema em potencial? Se o seu aplicativo armazena informações de perfil de usuário em cabeçalhos e esses valores são empregados em consultas dinâmicas, você pode estar vulnerável. Todos esses valores podem dar origem a ataques se você utiliza SQL dinâmico.

Páginas da Web que contêm funcionalidade de pesquisa podem ser vítimas excepcionalmente fáceis dos invasores, porque elas propiciam um método direto para que sejam feitas tentativas de injeção.

Isso pode dar a um invasor uma funcionalidade semelhante à de editor de consultas em um aplicativo vulnerável.

As pessoas têm se tornado muito mais conscientes dos problemas de segurança nos anos recentes, por isso, por padrão os sistemas em geral estão mais protegidos. Por exemplo, o procedimento de sistema xp_cmdshell está desabilitado em instâncias do SQL Server 2005 e de versões posteriores (inclusive no SQL Express). Mas não pense que isso significa que um invasor não possa executar comandos no seu servidor. Se a conta que o aplicativo usa para o banco de dados tem um nível de permissão alto o suficiente, um invasor pode simplesmente injetar o seguinte comando para reativar a opção:

    EXECUTE SP_CONFIGURE 'xp_cmdshell', '1'

Como evitar a injeção de SQL? Primeiro vejamos como não resolver esse problema. Uma abordagem bastante comum para corrigir aplicativos ASP clássicos era simplesmente substituir travessões e aspas. Infelizmente ela ainda é muito usada em aplicativos .NET, geralmente como o único meio de proteção:

string safeSql = "select * from users where loginId = " + userInput.Replace("—-", "");

safeSql = safeSql.Replace("'","''");

safeSql = safeSql.Replace("%","");

Essa abordagem pressupõe que você tenha:

  1. Protegido cada consulta devidamente com esses tipos de chamadas. Ela conta com a ajuda do desenvolvedor quando este se lembra de incluir essas verificações embutidas em todos os lugares, em vez de usar um padrão que proteja por padrão, mesmo depois de o desenvolvedor passar o final de semana codificando e sem cafeína.
  2. Verificado o tipo de cada parâmetro específico. Normalmente é apenas uma questão de tempo antes que o desenvolvedor se esqueça de verificar que um parâmetro de uma página da Web, por exemplo, é de fato um número e, depois, use esse número em uma outra consulta (para saber um ProductId, por exemplo) sem fazer verificações de cadeia de caracteres porque, afinal de contas, é um número. Mas e se um invasor alterar o ProductId e a cadeia de caracteres ficar assim:
URI: http://yoursite/product.aspx?productId=10

Produz

    select * from products where productid=10

E, então, o invasor injeta comandos como:

URI: http://yoursite/product.aspx?productId=10;select 1 col1 into #temp; drop table #temp;

Que produz

    select * from products where productid=10;select 1 col1 into #temp; drop table #temp;

Oh! Não! Você acabou de introduzir um valor em um campo de inteiro que não foi filtrado por uma função de cadeia de caracteres nem teve o tipo verificado. Isso se chama ataque de injeção direta porque não são necessárias aspas e a parte injetada é usada diretamente na consulta, sem aspas. Talvez você diga, “Sempre atento para que todos os dados sejam verificados”, mas isso coloca a responsabilidade de verificar cada parâmetro manualmente sobre o desenvolvedor, além de estar muito suscetível a erros. Por que não corrigir da forma certa usando um padrão melhor no aplicativo inteiro?

Então qual é a maneira correta de evitar a injeção de SQL? A maneira correta é muito fácil na maioria dos cenários de acesso a dados. O segredo é usar chamadas parametrizadas. Na verdade, você pode ter o SQL dinâmico, que é seguro desde que você torne essas chamadas parametrizadas. Estas são as regras básicas:

  1. Verifique se você está usando somente:
    • Procedimentos armazenados (sem SQL dinâmico)
    • Consultas parametrizadas (veja a Figura 2)

Figura 2 Consulta parametrizada

using (SqlConnection connection = new SqlConnection(  ConfigurationManager.ConnectionStrings[1].ConnectionString))

{

  using (SqlDataAdapter adapter = new SqlDataAdapter())

  {

    // Note we use a dynamic 'like' clause

    string query = @"Select Name, Description, Keywords From Product

                   Where Name Like '%' + @ProductName + '%'

                   Order By Name Asc";

    using (SqlCommand command = new SqlCommand(query, connection))

    {

      command.Parameters.Add(new SqlParameter("@ProductName", searchText));

      // Get data

      DataSet dataSet = new DataSet();

      adapter.SelectCommand = command;

      adapter.Fill(dataSet, "ProductResults");

      // Populate the datagrid

      productResults.DataSource = dataSet.Tables[0];

      productResults.DataBind();

    }

  }

}
  • Chamadas de procedimentos armazenados parametrizados (veja a Figura 3)

Figura 3 Chamada de procedimento armazenado parametrizado

//Example Parameterized Stored Procedure Call

string searchText = txtSearch.Text.Trim();

using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings[0].ConnectionString))

{

  using (SqlDataAdapter adapter = new SqlDataAdapter())

  {

    // Note: you do NOT use a query like: 

    // string query = "dbo.Proc_SearchProduct" + productName + ")";

    // Keep this parameterized and use CommandType.StoredProcedure!!

    string query = "dbo.Proc_SearchProduct";

    Trace.Write(string.Format("Query is: {0}", query));

    using (SqlCommand command = new SqlCommand(query, connection))

    {

      command.Parameters.Add(new SqlParameter("@ProductName", searchText));

      command.CommandType = CommandType.StoredProcedure;

      // Get the data.

      DataSet products = new DataSet();

      adapter.SelectCommand = command;

      adapter.Fill(products, "ProductResults");

      // Populate the datagrid.

      productResults.DataSource = products.Tables[0];

      productResults.DataBind();

    }

  }

}
  1.  2.   O SQL dinâmico em procedimentos armazenados deve ser chamadas parametrizadas para sp_executesql. Evite usar exec — ele não dá suporte para chamadas parametrizadas. Evite concatenas cadeias de caracteres com entrada do usuário. Veja a Figura 4.

Figura 4 Chamada parametrizada para sp_executesql



    /*
    
    This is a demo of using dynamic sql, but using a safe parameterized query
    
    */
    
    DECLARE @name varchar(20)
    
    DECLARE @sql nvarchar(500)
    
    DECLARE @parameter nvarchar(500)
    
    /* Build the SQL string one time.*/
    
    SET @sql= N'SELECT * FROM Customer  WHERE FirstName Like @Name Or LastName Like @Name +''%''';
    
    SET @parameter= N'@Name varchar(20)';
    
    /* Execute the string with the first parameter value. */
    
    SET @name = 'm%'; --ex. mary, m%, etc. note: -- does nothing as we would hope!
    
    EXECUTE sp_executesql @sql, @parameter,
    
                          @Name = @name;

Observação: devido à falta de suporte a parâmetro, NÃO use: exec 'select .. ' + @sql

  1.  3.   Não basta substituir travessões e aspas; não pense que você está protegido só por causa disso. Escolha e adote métodos consistentes de acesso a dados, como já descrito, que impeçam a injeção de SQL sem intervenção manual do desenvolvedor. Se você contar com uma rotina de escape e eventualmente se esquecer de chamá-la em um local, o local poderá ser invadido por hackers. Além disso, pode haver uma vulnerabilidade na forma como você implementa essa rotina, como no caso de um ataque de truncamento de SQL.
  2.  4.   Valide a entrada (consulte a próxima seção, Violação de parâmetros) através de verificações e transmissão de tipos; use expressões regulares para se limitar, por exemplo, a dados alfanuméricos ou extraia dados importantes somente de fontes conhecidas; não confie em dados provenientes da página da Web.
  3.  5.   Audite as permissões de objetos de banco de dados para limitar o escopo do usuário do aplicativo, limitando assim a área de superfície de ataque. Conceda permissões para atualizar, excluir e inserir, por exemplo, somente se o usuário tiver de executar essas operações. Cada aplicativo separado deve ter seu próprio logon no banco de dados, com permissões restritas. O meu projeto SQL Server Permissions Auditor, de código-fonte aberto, pode ajudar nisso; confira em sqlpermissionsaudit.codeplex.com.

É muito importante auditar as permissões de tabela se você usa consultas parametrizadas. As consultas parametrizadas exigem que um usuário ou uma função tenha permissões para acessar uma tabela. O seu aplicativo pode estar protegido contra injeção de SQL, mas e se outro aplicativo que não está protegido afetar o banco de dados? Um invasor pode começar a fazer consultas no seu banco de dados, por isso você deve se certificar de que cada aplicativo tem o seu próprio logon exclusivo e restrito. Você também deve auditar as permissões em relação a objetos de banco de dados, como exibições, procedimentos e tabelas. Os procedimentos armazenados exigem permissões somente em relação a eles próprios e não em relação à tabela, geralmente desde que não exista SQL dinâmico no procedimento armazenado, por isso é um pouco mais fácil gerenciar a segurança neles. Novamente o meu SQL Server Permissions Auditor pode ser útil.

Observe que o Entity Framework usa consultas parametrizadas em segundo plano e, portanto, está protegido contra injeção de SQL em cenários de uso normal. Alguns preferem mapear suas entidades para procedimentos armazenados em vez de abrir permissões de tabela para as consultas parametrizadas dinâmicas, mas existem argumentos válidos nos dois lados e essa é uma decisão que você precisa tomar. Se você estiver usando Entity SQL explicitamente, convém levar em consideração alguns outros aspectos de segurança em relação às suas consultas. Consulte a página “Considerações sobre segurança (Entity Framework)” da MSDN Library, em msdn.microsoft.com/library/cc716760.

Violação de parâmetros

O que é isso? A violação de parâmetros é um tipo de ataque em que parâmetros são alterados a fim de modificar a funcionalidade esperada do aplicativo. Os parâmetros podem estar em um formulário, em uma cadeia de caracteres de consulta, em cookies, no banco de dados, e assim por diante. Falarei sobre ataques que envolvem parâmetros baseados na Web.

Como ela é explorada? Um invasor altera parâmetros para fazer com que o aplicativo execute uma ação não pretendida. Vamos supor que você salve o registro de uma usuário lendo sua ID na cadeia de caracteres de consulta. Isso é seguro? Não. Um invasor pode violar uma URL no seu aplicativo de uma forma parecida com a mostrada na Figura 5.

An Altered URL
Figura 5 Uma URL alterada

Fazendo isso, o invasor poderia carregar uma conta de usuário que você não intencionava. E, de repente, o código de aplicativo semelhante ao mostrado a seguir passa a confiar cegamente nesse userId:

// Bad practice!

string userId = Request.QueryString["userId"];

// Load user based on this ID

var user = LoadUser(userId);

Há um modo melhor de fazer isso? Sim! É possível ler valores em uma fonte mais confiável, como a sessão de um usuário ou um provedor de associação ou de criação de perfil, em vez de confiar no formulário. 

Há várias ferramentas que facilitam muito a violação de outros itens além da cadeia de caracteres de consulta. Recomendo que você verifique algumas das barras de ferramentas de desenvolvedores de navegadores da Web disponíveis para ver os elementos ocultos nas suas páginas. Acho que você ficará surpreso ao descobrir o que está oculto e constatar como é fácil violar os seus dados. Considere a página "Editar Usuário" mostrada na Figura 6. Se você revelar os campos ocultos da página, verá uma ID de usuário inserida no formulário, pronta para uma violação (veja a Figura 7). Esse campo é usado como chave primária do registro desse usuário e sua violação altera qual registro é salvo de volta no banco de dados.

An Edit User Form
Figura 6 Um formulário de Editar Usuário

Revealing a Hidden Field on the Form
Figura 7 Revelando um campo oculto no formulário

Como evitar a violação de parâmetros? Não confie em dados fornecidos por usuários e valide os dados recebidos que baseiam a tomada de decisões. Normalmente você não se importa quando um usuário altera o segundo nome armazenado em seu perfil. Mas certamente se importaria se ele violasse a ID de formulário oculta que representa a chave de registro de usuário. Em casos como esse, é possível obter dados confiáveis de uma fonte segura no servidor e não da página da Web. Essas informações podem estar armazenadas na sessão do usuário após o logon ou no provedor de associação.

Por exemplo, uma abordagem preferível usa informações do provedor de associação em vez dos dados do formulário:

// Better practice

int userId = Membership.GetUser().ProviderUserKey.ToString();

// Load user based on this ID

var user = LoadUser(userId);

Agora que você compreendeu que os dados do navegador podem não ser confiáveis, vejamos alguns exemplos de como a validação desses dados pode ajudar a esclarecer um pouco as coisas. Estes são alguns cenários comuns de Web Forms:

// 1. No check! Especially a problem because this productId is really numeric.

string productId = Request.QueryString["ProductId"];

// 2. Better check

int productId = int.Parse(Request.QueryString["ProductId"]);

// 3.Even better check

int productId = int.Parse(Request.QueryString["ProductId"]);

if (!IsValidProductId(productId))

{

    throw new InvalidProductIdException(productId);

}

A Figura 8 mostra um cenário típico do MVC com ligação de modelo que faz conversões de tipos automáticas e básicas sem precisar converter parâmetros explicitamente.

Figura 8 Usando a ligação de modelo do MVC

[HttpPost]

[ValidateAntiForgeryToken]

public ActionResult Edit([Bind(Exclude="UserId")] Order order)

{

   ...

   // All properties on the order object have been automatically populated and 

   // typecast by the MVC model binder from the form to the model.

   Trace.Write(order.AddressId);

   Trace.Write(order.TotalAmount);

   // We don’t want to trust the customer ID from a page

   // in case it’s tampered with.

   // Get it from the profile provider or membership object

   order.UserId = Profile.UserId;

   // Or grab it from this location

   order.UserId = Membership.GetUser().ProviderUserKey.ToString();

   ...

   order.Save();}

   ...

   // etc.

}

A ligação de modelo é um excelente recurso do MVC (Model-View-Controller) que ajuda nas verificações de parâmetro, uma vez que as propriedades do objeto Order serão automaticamente preenchidas e convertidas em seus tipos definidos com base nas informações do formulário. É possível definir Data Annotations no modelo e incluir muitas validações diferentes. Apenas tenha cautela para limitar as propriedades que podem ser preenchidas e, mais uma vez, não confie nos dados de página de itens importantes. Uma boa regra prática é ter um ViewModel para cada View, assim você excluiria um UserId do modelo nesse exemplo de Edit.

Observe que uso o atributo [Bind(Exclude)] aqui para limitar o que o MVC está ligando ao meu Model para controlar no que confio/não confio. Isso garante que UserId não será proveniente dos dados de formulário e, portanto, não pode ser violado. A ligação de modelo e Data Annotations vão além do escopo deste artigo. Fiz uma pequena análise aqui para mostrar como os tipos de parâmetros podem funcionar em Web Forms e MVC.

Se tiver de incluir um campo ID na página da Web que você “confia”, acesse o link de extensões de segurança do MVC (mvcsecurity.codeplex.com) para ver um atributo que pode auxiliar nisso.

Conclusão

Neste artigo, apresentei duas das maneiras mais comuns de invasão de aplicativos. No entanto, como você pode ver, é possível evitar os ataques ou, pelo menos, limitá-los fazendo algumas pequenas alterações nos aplicativos. É claro que existem variações desses ataques e outras formas de exploração de aplicativos. Na próxima edição, falarei sobre dois outros tipos de ataques: scripts entre sites e falsificação de solicitação em diferentes locais.   

Adam Tuliper é arquiteto de software na Cegedim e desenvolve software há mais de 20 anos. Ele é palestrante da comunidade INETA local e participa com frequência de conferências e grupos de usuários do .NET. Conheça mais sobre o Adam no Twitter twitter.com/AdamTuliper, em seu blog completedevelopment.blogspot.com ou em secure-coding.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Barry Dorrans