.NET

16 fev, 2018

Cuidados de segurança importantes ao utilizar Dapper

Publicidade

Utilizado com frequência em aplicações .NET que exijam uma maior performance no acesso a bases relacionais, o Dapper é um micro-ORM que apresenta compatibilidade com o .NET Full (para projetos voltados a ambientes Windows) e o .NET Core (sendo este último multiplataforma, o que viabilizou seu uso em Windows, Linux e Mac simultaneamente).

Já abordei inclusive sua utilização em artigos anteriores:

Este framework foi também tema de um hangout do Canal .NET no segundo semestre de 2017 (foram discutidos na mesma ocasião o uso de Entity Framework e NHibernate):

Por mais que não conte com todos os recursos típicos de um ORM convencional como o Entity Framework e o NHibernate, o Dapper ainda é uma solução de utilização relativamente simples. E é justamente esta simplicidade que pode levar desenvolvedores descuidados a implementar aplicações com falhas de segurança.

Por envolver o acesso a dados provenientes de bases relacionais, a adoção do Dapper traz um problema comumente associado a este tipo de prática: a possibilidade de ataques de injeção de SQL (SQL Injection).

Nesta vulnerabilidade típica do uso de tecnologias relacionais, comandos SQL são inseridos juntamente com parâmetros em URLs ou, até mesmo, durante o preenchimento de campos de um formulário. Tais instruções SQL podem tanto retornar informações de maneira indevida, quanto produzir resultados indesejáveis, como a exclusão de estruturas de um banco de dados, ou alterações não planejadas.

O exemplo a seguir traz uma consulta numa base SQL Server a uma tabela contendo informações sobre cidades, utilizando como filtro, o estado destas localidades. Importante destacar que o método Query é uma extensão disponibilizada pelo próprio Dapper, com sua utilização acontecendo em conjunto com instâncias de classes que implementam IDbConnection (estrutura básica para conexão a bancos relacionais no ADO.NET):

List<Cidade> cidades;

using (SqlConnection conexao =
    new SqlConnection(
        config.GetConnectionString("ExemplosSeguranca")))
{
    cidades = conexao.Query<Cidade>(
        "SELECT * " +
        "FROM dbo.Cidades " +
        quot;WHERE Estado = '" + estado + "'").AsList();
}

A seguir, temos uma variação deste conjunto de instruções, empregando desta vez interpolação de strings na passagem do parâmetro que filtrará as cidades localizadas em um estado específico:

List<Cidade> cidades;

using (SqlConnection conexao =
    new SqlConnection(
        config.GetConnectionString("ExemplosSeguranca")))
{
    cidades = conexao.Query<Cidade>(
        "SELECT * " +
        "FROM dbo.Cidades " +
       quot;WHERE Estado = '{estado}'").AsList();
}

Em ambos os exemplos apresentados, há a possibilidade de injeção de instruções SQL. Embora algo simples a ser evitado, a geração de comandos SQL por meio da concatenação de strings com parâmetros representa um hábito comum entre muitos desenvolvedores. E como impedir que situações indesejadas ocorram? As seções a seguir trazem recomendações simples e bastante eficientes neste sentido.

Restringindo o acesso a um banco de dados

Está utilizando o Dapper em uma API REST que processará apenas requisições do tipo GET? O acesso a bancos de dados relacionais com um login com permissões somente para leitura evitará maiores problemas.

Haverá gravação e/ou exclusão de dados? O login em questão também deverá possuir permissão para a execução de operações como INSERT, UPDATE e DELETE.

Considerando uma tentativa de injeção de SQL na aplicação ASP.NET Core que faz uso das instruções C# apresentadas anteriormente, uma URL poderia conter o seguinte caminho e parâmetros:

/Cidades
estado=SP’;DROP%20TABLE%20dbo.Cidades;SELECT%201%20WHERE%20’’%20=%20′

ao invés de simplesmente /Cidades?estado=SP

Qual seria a consequência de se processar esta requisição, levando em conta o exemplo de consulta a cidades? A concatenação de strings produziria três instruções:

SELECT * FROM dbo.Cidades WHERE Estado = ‘SP’;DROP TABLE dbo.Cidades;SELECT 1 WHERE ‘’ = ‘’

Caso o usuário empregado na string de conexão possua direitos para a execução de um comando DROP TABLE, a tabela dbo.Cidades deixará então de existir.

Passagem de parâmetros

Além de comandos que podem alterar a estrutura e/ou modificar dados de uma base, há ainda a possibilidade de acesso a dados restritos ou que não façam sentido dentro de um determinado contexto.

Retomando o exemplo de consulta a cidades, a URL apresentada a seguir traz novamente um caso de SQL Injection. Desta vez, uma condição forçará a exibição de dados de dois estados diferentes (a ideia era restringir isto a apenas uma unidade federativa):

/Cidades?estado=SP’%20OR%20Estado%20=%20’MG

Como resultado disto, seria produzida a seguinte instrução SQL:

SELECT * FROM dbo.Cidades WHERE Estado = ‘SP’ OR Estado = ‘MG’

E em tela apareceriam dados de dois estados:

Uma consulta a dados financeiros que passasse por este mesmo tipo de problema poderia resultar em uma grave falha de segurança, sem sombra de dúvidas. E como evitar isto?

Recomenda-se que a passagem de parâmetros em Dapper faça uso de objetos anônimos. Cada nome de parâmetro será precedido pelo caractere @ (arroba), com as propriedades do objeto anônimo possuindo a mesma nomenclatura utilizada em sua expressão SQL (sem @, obviamente). O exemplo a seguir ilustra esta prática:

List<Cidade> cidades;

using (SqlConnection conexao =
    new SqlConnection(
        config.GetConnectionString("ExemplosSeguranca")))
{
    cidades = conexao.Query<Cidade>(
        "SELECT * " +
        "FROM dbo.Cidades " +
       quot;WHERE Estado = @SiglaEstado",
        new { SiglaEstado = estado }).AsList();
}

Uma outra solução ao fazer pesquisas cujo filtro seja a chave-primária de uma tabela é o Dapper.Contrib, um framework criado pelo time responsável pelo próprio Dapper e que simplifica a implementação de operações de CRUD.

Será necessário indicar ao Dapper.Contrib a tabela/view à qual uma classe se refere, além de sua primary key (com o atributo ExplicitKey para campos preenchidos via aplicação; no caso de uma chave auto-incremento usar o atributo Key). Um exemplo disso está na definição da classe Estado:

using Dapper.Contrib.Extensions;

namespace ExemploDapperContrib
{
    [Table("dbo.Estados")]
    public class Estado
    {
        [ExplicitKey]
        public string SiglaEstado { get; set; }
        public string NomeEstado { get; set; }
        public string NomeCapital { get; set; }
        public int IdRegiao { get; set; }
    }
}

Já na listagem seguinte, uma consulta aos dados de um estado foi realizada via Dapper.Contrib, através de uma chamada ao método Get (o qual receberá como parâmetro a sigla do estado):

[HttpGet("detalhes/{siglaEstado}")]
public Estado GetDetalhesEstado(string siglaEstado)
{
    using (SqlConnection conexao = new SqlConnection(
        _config.GetConnectionString("ExemplosDapper")))
    {
        return conexao.Get<Estado>(siglaEstado);
    }
}

E para concluir este artigo, deixo aqui um convite.

Que tal utilizar uma alternativa mais leve que o Management Studio para gerenciamento de recursos do SQL Server, Azure SQL Database e Azure SQL Data Warehouse? E se esta solução for compatível com Linux e macOS, além de Windows? E se tivermos ainda funcionalidades como exportação para Excel e JSON, integração com Git, além de suporte a ferramentas de linha de comando como PowerShell, Bash, sqlcmd e bcp?

A resposta a todas estas questões está no SQL Operations Studio, uma ferramenta open source disponibilizada pela Microsoft recentemente (Novembro/2017) em modo Preview.