Pular para o conteúdo
Categoria: Segurança da Informação14 min de leitura

O que é SQL Injection e como se proteger

Por Schematize Blog ·

Entenda como funciona a injeção de SQL, por que ela continua tão comum décadas depois e como preveni-la com queries parametrizadas e defesa em profundidade.

Imagine um formulário de login onde, em vez do nome de usuário, alguém digita um pedaço de código. Se a sua aplicação montar a consulta ao banco juntando texto, esse código pode ser executado pelo próprio banco de dados — e o atacante entra sem senha. Isso é SQL Injection, uma das falhas mais antigas e ainda mais perigosas da web.

Neste artigo você vai entender o mecanismo por trás do ataque, ver exemplos concretos do que pode dar errado e, principalmente, aprender as defesas que realmente funcionam. O foco é prático: ao final, você saberá escrever consultas seguras por padrão.

O que é SQL Injection

SQL Injection (SQLi) é uma vulnerabilidade em que entrada não confiável do usuário é interpretada como parte de um comando SQL. A raiz do problema é a mistura entre código (a estrutura da consulta) e dados (os valores fornecidos pelo usuário). Quando os dois se confundem, o atacante consegue alterar a lógica da consulta.

Halfond, Viegas e Orso (2006) propõem uma classificação dos tipos de ataque e contramedidas, mostrando que o SQLi não é um truque único, mas uma família de técnicas. Apesar de bem compreendido há quase duas décadas, ele permanece no topo das falhas mais reportadas, aparecendo na categoria de injeção do OWASP Top 10 explicado: as 10 maiores falhas de segurança web.

Vale entender por que essa falha sobrevive há tanto tempo. Não é por falta de solução — a defesa é conhecida e simples. É porque a forma intuitiva e errada de montar consultas (juntando texto) funciona perfeitamente nos testes do dia a dia, onde ninguém digita uma aspa maliciosa. A vulnerabilidade fica dormente até que alguém com más intenções a encontre. O código "passa" em todos os testes funcionais e só falha no teste que importa: o do atacante.

Por que ele acontece: o erro da concatenação

Quase todo caso de SQLi nasce de uma consulta construída por concatenação de strings. Veja um exemplo clássico em pseudocódigo:

# VULNERÁVEL — nunca faça isso
usuario = request.get("usuario")
senha = request.get("senha")
query = "SELECT * FROM usuarios WHERE usuario = '" + usuario + "' AND senha = '" + senha + "'"
db.execute(query)

Se o usuário digitar no campo de usuário:

' OR '1'='1

A consulta final vira:

SELECT * FROM usuarios WHERE usuario = '' OR '1'='1' AND senha = '...'

Como '1'='1' é sempre verdadeiro, a condição de autenticação se quebra e o atacante pode entrar. O banco fez exatamente o que foi mandado — o problema é que o texto do usuário virou parte do comando.

Um exemplo ainda mais grave: o comentário

Outra técnica comum usa o marcador de comentário do SQL (--) para descartar o resto da consulta. Se o atacante digitar no campo de usuário:

admin' --

A consulta vira:

SELECT * FROM usuarios WHERE usuario = 'admin' --' AND senha = '...'

Tudo depois de -- é ignorado pelo banco como comentário. Resultado: o atacante entra como admin sem precisar da senha, porque a verificação da senha foi literalmente apagada da consulta. Esse é o tipo de falha que transforma um formulário de login banal num portão escancarado.

O que um atacante consegue fazer

O impacto vai muito além de pular um login. Dependendo do contexto, um SQLi permite:

    Por isso o SQLi é considerado de alta severidade: uma única consulta vulnerável pode comprometer toda a base de dados. Não é exagero dizer que um único endpoint mal escrito pode vazar a empresa inteira — e há inúmeros casos públicos de grandes vazamentos cuja origem foi exatamente isso.

    Tipos principais de SQL Injection

    Conhecer as variações ajuda a entender por que filtros ingênuos falham:

      Essa diversidade mostra por que "remover aspas" não basta: há muitos caminhos para injetar comandos, muitos deles independentes de aspas (por exemplo, em campos numéricos onde a aspa nem aparece).

      Por que a "blocklist" não funciona

      Uma tentação comum de quem começa é tentar filtrar palavras perigosas — bloquear entradas que contenham SELECT, DROP, --, OR, e assim por diante. Essa abordagem, chamada de blocklist (lista de negação), é frágil por princípio:

        A segurança não pode depender de adivinhar tudo o que é perigoso. A abordagem correta inverte a lógica: em vez de tentar identificar entrada maliciosa, garantimos que nenhuma entrada possa ser interpretada como código, qualquer que seja seu conteúdo. É o que as queries parametrizadas fazem.

        A defesa principal: queries parametrizadas

        A solução definitiva é separar código de dados usando queries parametrizadas (também chamadas de prepared statements). Nelas, você define a estrutura da consulta com marcadores e passa os valores separadamente. O driver do banco garante que esses valores nunca sejam interpretados como comando:

        # SEGURO — valores vão como parâmetros, não como texto da query
        query = "SELECT * FROM usuarios WHERE usuario = %s AND senha_hash = %s"
        db.execute(query, (usuario, senha_hash))

        Repare na diferença conceitual: na versão segura, mesmo que o usuário digite ' OR '1'='1, isso será tratado como um nome de usuário literal que simplesmente não existe. O banco procura por alguém chamado exatamente assim e não encontra. A estrutura da consulta permanece intacta.

        O mecanismo por baixo é importante: com prepared statements, a aplicação envia primeiro a estrutura da consulta (com os marcadores) ao banco, que a compila e planeja. Só depois os valores são enviados separadamente. Como a estrutura já foi fixada antes de os dados chegarem, é fisicamente impossível que um dado altere a lógica — o banco já decidiu o que vai executar.

        Como fica em diferentes linguagens

        A sintaxe varia, mas o princípio é idêntico. Em Java com JDBC:

        PreparedStatement stmt = conn.prepareStatement(
            "SELECT * FROM usuarios WHERE id = ?");
        stmt.setInt(1, idDoUsuario);
        ResultSet rs = stmt.executeQuery();

        Em Node.js com um driver de PostgreSQL:

        await client.query(
          'SELECT * FROM usuarios WHERE email = $1',
          [emailDoUsuario]
        );

        Em PHP com PDO:

        $stmt = $pdo->prepare('SELECT * FROM usuarios WHERE email = :email');
        $stmt->execute(['email' => $emailDoUsuario]);

        Quase toda linguagem e framework oferece esse mecanismo nativamente. Use-o sempre, mesmo para valores que parecem inofensivos. Um campo "numérico" vindo de um formulário ainda é texto controlado pelo atacante até prova em contrário.

        Defesas complementares

        Queries parametrizadas resolvem a maioria dos casos, mas uma postura de defesa em profundidade acrescenta camadas. A ideia é que, se uma barreira falhar, outras ainda contenham o estrago:

          A combinação dessas medidas com a separação criptográfica de senhas — assunto de O que é criptografia? Simétrica, assimétrica e hashing — garante que, mesmo num vazamento, os dados estejam protegidos.

          O caso especial das colunas dinâmicas

          Há um cenário que confunde muita gente: e quando o usuário precisa, legitimamente, escolher por qual coluna ordenar ou filtrar? Nomes de coluna e de tabela não podem ser passados como parâmetros — prepared statements só parametrizam valores, não identificadores estruturais. A solução não é concatenar a entrada; é mapeá-la contra uma lista fechada:

          # Lista de permissão: a entrada do usuário só pode escolher um item conhecido
          COLUNAS_PERMITIDAS = {"nome": "nome", "preco": "preco", "data": "criado_em"}
          
          coluna = COLUNAS_PERMITIDAS.get(request.get("ordenar"), "nome")  # fallback seguro
          query = f"SELECT * FROM produtos ORDER BY {coluna}"  # coluna veio de lista fixa
          db.execute(query)

          Aqui o texto do usuário nunca toca a consulta diretamente: ele apenas seleciona uma chave de um dicionário controlado por você. Qualquer valor fora da lista cai no fallback. Esse padrão fecha a única brecha que as queries parametrizadas, sozinhas, não cobrem.

          Anatomia de um ataque cego, passo a passo

          Para entender por que o SQLi é tão poderoso mesmo quando a aplicação não mostra dados, vale acompanhar o raciocínio de um ataque cego (blind). Suponha uma página que mostra "produto encontrado" ou "produto não encontrado" conforme uma consulta. O atacante não vê dados, só esse sinal binário — e isso basta.

          Ele começa testando se há injeção, anexando uma condição sempre verdadeira e depois uma sempre falsa, e observando se a resposta muda:

          /produto?id=10 AND 1=1   → "encontrado"  (condição verdadeira)
          /produto?id=10 AND 1=2   → "não encontrado" (condição falsa)

          Se a página reage diferente, há injeção. A partir daí, o atacante faz perguntas de sim/não ao banco, extraindo informação bit a bit:

          /produto?id=10 AND (SELECT SUBSTRING(senha,1,1) FROM usuarios WHERE id=1) = 'a'

          Se a página responde "encontrado", o primeiro caractere da senha é a; se não, ele tenta b, e assim por diante. Repetindo o processo para cada posição, é possível reconstruir uma senha inteira sem que a aplicação jamais tenha "mostrado" um único dado. A variante time-based faz o mesmo usando SLEEP quando a resposta da página é idêntica nos dois casos: se a página demora, a resposta à pergunta foi "sim".

          Esse exemplo deixa claro por que esconder a saída não é defesa. A única defesa real é impedir que a entrada vire comando, e isso só as queries parametrizadas garantem.

          SQL Injection no contexto de outras falhas

          SQLi raramente aparece sozinho. Ele costuma ser combinado com outras vulnerabilidades. Por exemplo, dados extraídos por um SQLi podem ser usados para falsificar identidade, conectando-se a problemas de Autenticação vs autorização: qual a diferença?. Da mesma forma, a injeção de scripts no navegador segue uma lógica parecida de mistura entre código e dados — veja O que é XSS (Cross-Site Scripting)?.

          Na verdade, SQLi é apenas um membro da grande família de falhas de injeção, que inclui injeção de comandos do sistema operacional, injeção de LDAP, injeção de NoSQL e outras. Todas compartilham a mesma raiz conceitual: um interpretador recebendo dados não confiáveis misturados com instruções. Entender essa família torna mais fácil reconhecê-la em qualquer camada — e a defesa é sempre a mesma ideia: separar instrução de dado.

          Um checklist prático de prevenção

          Quando for revisar uma base de código em busca de SQLi, percorra esta lista mental:

            Esse roteiro transforma o conhecimento abstrato em verificação concreta, do tipo que cabe numa revisão de código ou numa auditoria.

            Como testar suas defesas de forma ética

            A melhor forma de internalizar o risco é ver o ataque funcionando em um ambiente controlado e autorizado. Nunca teste sistemas de terceiros sem permissão explícita; isso é crime. Use laboratórios próprios ou plataformas feitas para treino. Demonstramos um passo a passo seguro em Explorando SQL Injection na prática (ético), com aplicações deliberadamente vulneráveis pensadas para aprendizado.

            No nível automatizado, ferramentas de análise estática (SAST) podem sinalizar concatenação de SQL no seu código durante o desenvolvimento, e testes de segurança automatizados podem tentar injeções conhecidas contra a aplicação rodando. Incluir essas verificações na esteira de CI ajuda a pegar o problema antes que ele chegue à produção.

            Perguntas frequentes

            Usar um ORM me deixa 100% seguro contra SQLi? Quase, mas não totalmente. ORMs parametrizam por padrão, o que cobre o caso comum. O risco volta sempre que você usa a API de "SQL bruto" do ORM concatenando entrada, ou quando monta dinamicamente nomes de coluna. Saber onde o ORM para de proteger é essencial.

            Escapar as aspas resolve? É frágil e desencorajado. Escape manual erra em casos de borda (diferentes codificações, campos numéricos, diferentes bancos) e é fácil esquecer em algum ponto. Prepared statements são a abordagem robusta porque não dependem de você lembrar de escapar nada.

            SQL Injection ainda é relevante hoje? Sim. Apesar de a defesa ser conhecida há décadas, a falha continua aparecendo no OWASP Top 10 e em vazamentos reais, porque a concatenação insegura ainda é escrita todos os dias, especialmente em código rápido e em queries dinâmicas complexas.

            Validação de entrada substitui parametrização? Não. Validação reduz a superfície e melhora a qualidade dos dados, mas não foi feita para impedir injeção. A parametrização é a defesa estrutural; a validação é uma camada adicional.

            Conclusão

            SQL Injection persiste não por ser difícil de evitar, mas porque a concatenação de strings é tentadora e parece funcionar nos testes do dia a dia. A regra de ouro é simples e absoluta: nunca monte consultas juntando texto do usuário. Use queries parametrizadas em todo acesso ao banco, trate nomes de coluna dinâmicos com listas de permissão, e reforce com ORMs, validação, menor privilégio e tratamento discreto de erros. Teste suas defesas em ambientes controlados e autorizados. Adotar essa disciplina elimina, de uma vez, uma das categorias de falha mais antigas e devastadoras da web.

            Referências

              Leituras relacionadas

              Nenhum comentário ainda

              Seja o primeiro a comentar.

              Deixe seu comentário

              Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

              Entrar com Canverly