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:
- Dapper: exemplos de utilização em ASP.NET Core e .NET Full
- Dapper: exemplos em .NET Core 2.0 e ASP.NET Core 2.0
- Dapper: relacionamentos Um-para-Um e Um-para-Muitos (exemplos em ASP.NET Core)
- Dapper + .NET Core 2.0: exemplos utilizando PostgreSQL e MySQL
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.