A Injeção de SQL, ou em inglês SQL Injection, é uma vulnerabilidade que permite que algum usuário malicioso possa executar uma instrução em SQL dentro da aplicação, aproveitando uma brecha em uma consulta que permita acesso ao banco de dados.
Para demonstrar um exemplo desta vulnerabilidade, vamos criar um banco de dados de vendas, onde temos nossa tabela de usuários com alguns informações.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
USE [master]; GO CREATE DATABASE [VendasDb]; GO USE [VendasDb]; GO CREATE TABLE [dbo].[Usuario] ( [Id] INT IDENTITY(1,1) NOT NULL PRIMARY KEY, [Usuario] VARCHAR(50) NOT NULL, [Senha] VARCHAR(50) NOT NULL ); GO INSERT INTO [dbo].[Usuario]([Usuario], [Senha]) VALUES ('tiagopariz', '123456'), ('jonatanvies', '789012'), ('alineloas', '345678'); GO |
Abra o Visual Studio Code, e crie uma pasta chamada EvitandoAInjecaoDeSQL, e dentro dela outra chamada src. Em EvitandoAInjecaoDeSQL crie uma solução usando o comando dotnet new sln e logo em seguida abra a pasta src, adicione e logo abra um nova pasta chamada EvitandoAInjecaoDeSQL.Web e adicione um projeto do tipo MVC usando o comando dotnet new mvc, volte até a pasta EvitandoAInjecaoDeSQL e adicone o projeto na solução usando o comando dotnet sln add .\src\EvitandoAInjecaoDeSQL.Web\EvitandoAInjecaoDeSQL.Web.csproj.
Adicione um controller na pasta Controllers do projeto de MVC chamado ContaController.cs e uma view chamada Login.cshtml na pasta Views/Conta.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
using System; using Microsoft.AspNetCore.Mvc; using System.Data.SqlClient; namespace EvitandoAInjecaoDeSQL.Web.Controllers { public class ContaController : Controller { [HttpGet] public ActionResult Login() { return View(); } [HttpPost] public ActionResult Login(string usuario, string senha) { var connectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=VendasDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"; var consulta = "SELECT COUNT(*) FROM Usuario WHERE Usuario = '" + usuario + "' AND Senha = '" + senha + "'"; try { using (var conexao = new SqlConnection(connectionString)) { conexao.Open(); using (SqlCommand comando = new SqlCommand(consulta, conexao)) { var resultado = (int)comando.ExecuteScalar(); if (resultado > 0) ViewBag.Mensagem = "Login efetuado com sucesso"; else ViewBag.Mensagem = "Falha no login"; } } } catch (Exception e) { ViewBag.Mensagem = "Erro: " + e.Message; } return View(); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<h2>Login</h2> @using (Html.BeginForm("Login", "conta", FormMethod.Post, new { enctype = "multipart/form-data" })) { <label>Usuario: </label> <input type="text" id="usuario" name="usuario" /><br /> <label>Senha: </label> <input type="text" id="senha" name="senha" /><br /> <input type="submit" value="Login" /><br /> <label>@ViewBag.Mensagem</label> } |
A tela de login deve ser semelhante a esta, após abrir o site e digitar uma senha válida:
O nosso site está funcionando, então agora vamos aplicar o SQL Injection para verificar a vulnerabilidade. Como vimos, o campo usuário e senha só efetua login se for digitado um nome de usuário e uma senha correta. Mas vamos tentar burlar isto.
Para entdenr o que acontece no lado do banco de dados ao digitar o usuário e senha da captura de tela, vamos analisar a intrução SQL montada pelo aplicativo:
1 2 3 |
SELECT COUNT(*) FROM Usuario WHERE Usuario = 'tiagopariz' AND Senha = '123456' |
Se executarmos a instrução acima no SQL Server, temos o resultado 1, que é a informação que permite que o sistema permita o acesso.
Mas esta instrução é facilmente alterada usando alguns caracteres de ‘ (apóstrofo), fazendo a consulta ficar assim:
1 2 3 |
SELECT COUNT(*) FROM Usuario WHERE Usuario = 'tiagopariz' AND Senha = '' OR '1' = '1' |
Que retorna um valor positivo, mesmo sem informar a senha.
Mas como isto é possível sem editar o SQL que está no código? Simples, pois a forma a instrução foi montada, permite que seja alterada, pois o SELECT é uma concatenação dos campos que são enviados no post e a o corpo da instrução, o C# não sabe o que é filtro e o que é comando de banco, ele monta primeiro uma string e depois executa no banco. Sendo assim, vamos recriar esta consulta direto no campo de senha.
Antes, tente digitar uma senha errada, para conferir se formulário está funcionando e acessando os dados corretamente.
Agora vamos explorar a vulnerabilidades, digitando no campo de senha ‘ OR ‘1’ = ‘1 e clique em login.
E acabamos de ser invadidos por uma falha bem simples no site, pois é possivel “editar” a instrução SQL antes de enviar. Este tipo de falha, pode ser tão grave que é até pode permitir acessar e alterar informações do próprio sistema operacional, dependendo da configuração do banco de dados.
Mas como consertamos este caso específico? A solução a seguir vai resolver este tipo de abertura, mas há outros tipos de falahas que podem precisar uma solução mais avançada. O que vamos fazer é passar parâmetros para o banco de dados em vez de montar todo o sql de uma vez, para isso, em vez de usar uma concatenação, mas nomer estes dados usando um @ (arroba antes) antes, então, para isto, vou comentar o código vulnerável do arquivo ContaController.cs e adicionar a forma mais segura, como segue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
using System; using Microsoft.AspNetCore.Mvc; using System.Data.SqlClient; namespace EvitandoAInjecaoDeSQL.Web.Controllers { public class ContaController : Controller { [HttpGet] public ActionResult Login() { return View(); } [HttpPost] public ActionResult Login(string usuario, string senha) { var connectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=VendasDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"; // Código com a vunerabilidade devido a concatenação //var consulta = "SELECT COUNT(*) FROM Usuario WHERE Usuario = '" + usuario + "' AND Senha = '" + senha + "'"; // Forma mais segura, montando o SELECT que espera os parâmetros @usuario e @senha var consulta = "SELECT COUNT(*) " + "FROM Usuario " + "WHERE " + "Usuario = @usuario AND " + "Senha = @senha;"; try { using (var conexao = new SqlConnection(connectionString)) { conexao.Open(); using (SqlCommand comando = new SqlCommand(consulta, conexao)) { // Passando os parâmetros de SQL sem a necessidade de concatenar comando.Parameters.Add(new SqlParameter("@usuario", usuario)); comando.Parameters.Add(new SqlParameter("@senha", senha)); var resultado = (int)comando.ExecuteScalar(); if (resultado > 0) ViewBag.Mensagem = "Login efetuado com sucesso"; else ViewBag.Mensagem = "Falha no login"; } } } catch (Exception e) { ViewBag.Mensagem = "Erro: " + e.Message; } return View(); } } } |
Com isto, temos um código mais seguro. Outra solução, seria usar frameworks ORM, como Entity Framework ou NHibernate.
O projeto completo está disponível no meu GitHub:
https://github.com/tiagopariz/EvitandoAInjecaoDeSQL
Obrigado e um bom dia a todos.