Pular para o conteúdo
Novo

DeepSeek V4.1 Flash grátis por 7 dias: 10M tokens por dia após qualquer recarga. Até 30/11.

Como funciona

SQL Injection explicada com o OWASP Juice Shop

Como a SQL Injection muda o sentido de uma consulta, por que consultas parametrizadas resolvem o problema e um laboratório com Juice Shop e sqlite3.

Intermediário10 min de leituraPor NotHoly

SQL Injection acontece quando a aplicação monta uma consulta ao banco juntando o próprio texto SQL com dados que vieram do usuário. O banco recebe uma única string e não tem como saber quais caracteres o desenvolvedor escreveu e quais o usuário digitou. Quando o texto do usuário chega ao parser como SQL, ele pode mudar o que a consulta significa. A consulta pode devolver outras linhas, pular uma verificação, alterar dados ou, nos piores casos, alcançar coisas que a aplicação nunca deveria expor.

Este artigo explica o mecanismo, mostra a correção que ataca a causa e traz duas partes práticas. A primeira é um desafio de login no OWASP Juice Shop, que você executa na sua própria máquina. A segunda é um script curto em Python com sqlite3 que reproduz o mesmo erro e a correção. Se você já leu OWASP Top 10 para desenvolvedores, essa categoria já está na sua cabeça. Aqui vamos a um item dessa lista e descemos até o texto da consulta.

O que é SQL Injection

A definição de CWE para essa fraqueza, o CWE-89, descreve o problema com clareza: o software monta parte de um comando SQL a partir de uma entrada e não neutraliza a sintaxe SQL que vem dentro dessa entrada. Assim, o dado é interpretado como comando.

Três palavras explicam a definição: monta, entrada e interpretado. O erro não está em a aplicação conversar com um banco de dados. O erro está em a aplicação montar o texto da consulta por conta própria, e algumas das peças vêm de fora.

Por que isso importa

Uma consulta de login é o menor exemplo útil. Ela verifica um nome e uma senha. Se um atacante consegue mudar a consulta, consegue mudar quem é autenticado. O mesmo defeito em uma caixa de busca pode devolver linhas que o usuário nunca deveria ver. Em uma página administrativa, pode alterar preços, papéis ou dados armazenados. Muitos desses erros começam com um único caractere de aspas que chega ao banco sem escape, e é assim que quem testa costuma confirmar o problema pela primeira vez.

A injeção também não exige um payload sofisticado. Desenvolvedores costumam encontrá-la com uma entrada que faz a aplicação travar ou devolver um erro estranho. Uma mensagem de erro que menciona sintaxe SQL é um forte sinal de que a entrada chegou à consulta.

Como a consulta muda de sentido

Comece com um modelo que um desenvolvedor poderia escrever:

sql
SELECT * FROM users WHERE name = '<name>' AND pw = '<pw>'

Imagine que o campo de nome recebe admin' -- e o campo de senha recebe wrong. Depois que a aplicação coloca essas strings no modelo, o banco interpreta isto:

sql
SELECT * FROM users WHERE name = 'admin' --' AND pw = 'wrong'

Leia passo a passo. A primeira aspa depois de admin fecha o literal de string que o desenvolvedor abriu. Em seguida, -- inicia um comentário no SQLite, e na maioria dos dialetos SQL. Tudo o que vem depois é ignorado, inclusive a condição da senha. O banco recebe uma consulta válida que pede a linha cujo nome é admin, e nada mais.

Quatro coisas acontecem em ordem:

  1. A aplicação copia os caracteres do usuário para o texto da consulta.
  2. O parser do banco lê a string inteira como SQL. Ele não distingue quais caracteres vieram do desenvolvedor e quais vieram do usuário.
  3. Uma aspa do usuário encerra o literal antes do previsto. O texto depois dela é lido como palavras-chave, operadores ou comentários SQL.
  4. A condição que o desenvolvedor escreveu deixa de ser a condição que o banco executa.

Interpolação com f-string, concatenação de strings, formatação com %, .format() e template strings em JavaScript têm o mesmo defeito. A linguagem não importa. O que importa é o método de juntar texto e SQL.

A correção na raiz: consultas parametrizadas

A parametrização separa o texto SQL dos valores. Você envia a consulta com marcadores e os valores chegam ao banco pelo driver como dados. O banco nunca os interpreta como SQL, então uma aspa dentro de um valor não tem significado especial.

A documentação do sqlite3 do Python recomenda usar marcadores em vez de formatação de string ao vincular valores Python a instruções SQL, justamente para evitar SQL Injection. O OWASP SQL Injection Prevention Cheat Sheet indica consultas parametrizadas como a defesa primária pelo mesmo motivo: a estrutura da consulta é definida separadamente dos dados.

A validação continua importante, mas como segunda camada. O que corrige a causa é a vinculação de parâmetros. Se você valida um valor e depois o concatena na consulta, o erro continua ali.

Mão na massa: resolva o desafio Login Admin

O Juice Shop é uma aplicação web vulnerável de propósito, mantida pela OWASP para treinamento. Execute-a apenas na sua máquina e mantenha a porta presa ao endereço de loopback, para que nenhum outro computador consiga alcançá-la.

Suba o laboratório em localhost

bash
docker run -d -p 127.0.0.1:3000:3000 bkimminich/juice-shop

O prefixo 127.0.0.1: publica a porta somente na interface de loopback. Se a porta 3000 já estiver em uso, o Docker falha com um erro como port is already allocated. Na máquina de teste usada para este artigo, outro contêiner local já ocupava a porta 3000 e o comando falhou por isso. Escolha uma porta livre, por exemplo 127.0.0.1:3001:3000, e use essa porta nos passos seguintes.

Confirme que a aplicação responde e veja a versão:

bash
curl -s http://127.0.0.1:3000/rest/admin/application-version

Na máquina de teste, a resposta foi:

text
{"version":"20.3.0"}

Encontre o ponto de injeção

O guia oficial do Juice Shop, o Pwning OWASP Juice Shop, não mostra o payload deste desafio. Ele dá dicas: a descrição do desafio indica qual formulário atacar, um ataque direcionado funciona bem se você já souber o e-mail do administrador, e um padrão de injeção comum pode funcionar mesmo sem esse dado. Siga essas dicas. Comece pelo formulário de login e coloque uma aspa no campo de e-mail. Uma senha errada normal deve gerar um erro. Uma consulta quebrada costuma gerar outra resposta.

Na máquina de teste, uma senha errada para o e-mail do administrador, sem nenhuma injeção, devolveu:

text
Invalid email or password.

com status HTTP 401. Esse é o ponto de referência. Depois, a mesma requisição com aspas e comentário no campo de e-mail:

bash
curl -s -X POST http://127.0.0.1:3000/rest/user/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]'\''--","password":"wrong"}'

A resposta contém um objeto authentication com um campo token. A senha estava errada, mas o servidor emitiu um token de sessão. Decodificando a parte do meio do JSON Web Token, que está em base64url, aparece "role":"admin". A resposta veio com HTTP 200 na máquina de teste.

Verifique o estado do desafio pela API do laboratório:

bash
curl -s "http://127.0.0.1:3000/api/Challenges/?name=Login%20Admin"

Na máquina de teste, o desafio voltou como resolvido, com dificuldade 2. O placar é a fonte da verdade para o desafio. O seu resultado deve bater com essa mesma consulta.

O guia de companhia e o código-fonte do Juice Shop são os lugares para confirmar a consulta exata por trás desse formulário. Leia-os depois de ter o resultado, para entender o mecanismo em vez de só copiar um payload.

Pare o laboratório

bash
docker ps
docker stop <nome-do-contêiner>

Use o nome ou o ID do contêiner que o docker ps mostrar. Fechar a aba do navegador não para o contêiner.

Reproduza o erro e a correção em Python

Salve este conteúdo como sqli_demo.py. Ele usa apenas a biblioteca padrão. O script cria uma tabela users pequena em memória e expõe duas funções de login. Uma monta a consulta com f-string. A outra usa marcador.

python
import sqlite3

con = sqlite3.connect(":memory:")
cur = con.cursor()
cur.execute("CREATE TABLE users (name TEXT, pw TEXT)")
cur.execute("INSERT INTO users VALUES ('alice', 'correct-horse'), ('admin', 'S3cret!')")

def login_unsafe(user, pw):
    query = f"SELECT * FROM users WHERE name='{user}' AND pw='{pw}'"
    print("  built:", query)
    return cur.execute(query).fetchall()

def login_safe(user, pw):
    return cur.execute('SELECT * FROM users WHERE name=? AND pw=?', (user, pw)).fetchall()

if __name__ == "__main__":
    print("same valid login, unsafe:", login_unsafe("admin", "S3cret!"))
    print("same valid login, safe:  ", login_safe("admin", "S3cret!"))
    print("crafted input, unsafe:")
    print("  result:", login_unsafe("admin' --", "wrong"))
    print("crafted input, safe:")
    print("  result:", login_safe("admin' --", "wrong"))

Execute. No Windows, com o Python instalado como python, o comando é python sqli_demo.py. Na máquina de teste, o python3 não estava no PATH, então use python. A saída observada na máquina de teste com Python 3.12.10:

text
same valid login, unsafe: [('admin', 'S3cret!')]
same valid login, safe:   [('admin', 'S3cret!')]
crafted input, unsafe:
  built: SELECT * FROM users WHERE name='admin' --' AND pw='wrong'
  result: [('admin', 'S3cret!')]
crafted input, safe:
  result: []

Compare as duas metades. Para um login normal, as duas funções devolvem a mesma linha, então a versão insegura parece correta. É por isso que esse erro sobrevive a revisões de código. Com entrada maliciosa, a função insegura imprime a consulta que o banco recebeu. O comentário remove a verificação da senha, e a linha do administrador volta mesmo com a senha errada. A função segura trata admin' -- como um nome literal. Nenhum usuário tem esse nome, então devolve uma lista vazia.

O script guarda senhas em texto puro porque é uma demonstração. Sistemas reais guardam um hash de senha com salt e comparam hashes.

Onde ORMs e helpers ainda erram

Um ORM não torna o erro impossível. Ele oferece um padrão seguro para consultas comuns e um conjunto de saídas de emergência para todo o resto. Estes são os pontos onde o erro costuma voltar:

  • Métodos de SQL bruto. Chamadas como raw, text, extra ou um execute simples aceitam uma string. Se você monta essa string com f-string ou concatenação, voltou ao padrão inseguro.
  • Nomes de tabela e de coluna. Marcadores vinculam valores. Eles não vinculam identificadores. Uma coluna de ORDER BY ou um nome de tabela escolhido pelo usuário precisa passar por uma lista de permissões que mapeia a escolha para nomes que você mesmo escreveu. O OWASP recomenda a mesma abordagem para escolhas estruturais fixas, como a direção da ordenação.
  • Listas IN dinâmicas. O número de marcadores muda com a entrada. Gere a quantidade certa de ? no seu próprio código e passe os valores como tupla. Não junte os valores do usuário no texto.
  • Funções auxiliares de filtro. Uma função que recebe um filtro em string e o acrescenta a uma cláusula WHERE é um ponto de concatenação, mesmo quando o resto da aplicação usa o ORM corretamente.

Erros comuns

Bloquear palavras. Um filtro que recusa OR, SELECT, aspas ou -- é fácil de contornar com troca de maiúsculas, codificações e outros estilos de comentário. O cheat sheet da OWASP é claro: validação não substitui a vinculação de parâmetros. Use a vinculação primeiro. A validação serve para as escolhas estruturais fixas citadas acima, não como defesa principal.

Escapar aspas na mão. Cada driver tem as próprias regras, e um caso esquecido basta. Deixe o driver fazer esse trabalho.

Confiar só no mínimo privilégio. Uma conta de banco com menos permissões limita o que um atacante faz depois da injeção. Ela não impede a injeção. A OWASP trata o mínimo privilégio como controle complementar e diz que contas de aplicação nunca devem ter direitos de DBA ou administrador. Faça as duas coisas: parametrize e dê à aplicação o menor conjunto de permissões de que ela precisa.

Parametrizar valores e esquecer o resto. Uma equipe converte a cláusula WHERE para marcadores, mas concatena a coluna de ordenação sem pensar nela. Revise todo lugar em que texto entra em uma consulta, não só o mais óbvio.

Testar só com entrada normal. O erro aparece só quando a entrada contém sintaxe SQL. Adicione um teste que envie o valor malicioso e espere um resultado vazio ou uma requisição recusada.

Como ir além

Leia a seção da documentação do sqlite3 do Python sobre marcadores e depois experimente o mesmo padrão com o driver do seu projeto. Leia o OWASP SQL Injection Prevention Cheat Sheet inteiro, principalmente as seções sobre procedimentos armazenados e identificadores dinâmicos.

Volte ao laboratório do Juice Shop e resolva os outros desafios de SQL com o mesmo cuidado. Para cada um, anote a consulta que você acredita que o servidor executa e depois confira no código-fonte.

Adicione um teste de regressão ao seu código. O teste deve enviar admin' -- ou um valor parecido para cada função de consulta que aceite entrada do usuário e afirmar que a função não devolve linhas que não deveria devolver.

Procure no seu código f-strings, formatação com %, .format() e concatenação de strings perto de execute, raw, text ou extra. Cada ocorrência é um ponto para revisar, não um bug confirmado.

Ferramentas de análise estática conseguem sinalizar alguns desses padrões, e um modelo de linguagem pode ajudar a ler construtores de consulta desconhecidos. Nenhum dos dois substitui o teste que mostra se a sua consulta se comporta corretamente. A API da NotHoly é uma opção para esse tipo de revisão, e ela só é útil na medida em que o teste confirma o resultado.

O próximo artigo desta série trata de Cross-Site Scripting (XSS), que tem uma forma parecida: texto não confiável acaba sendo interpretado como algo que não deveria ser.

Fontes

Fontes

  1. OWASP SQL Injection Prevention Cheat Sheet
  2. MITRE CWE-89: Improper Neutralization of Special Elements used in an SQL Command
  3. OWASP Pwning OWASP Juice Shop companion guide
  4. Python sqlite3 DB-API 2.0 interface for SQLite databases

Mais em Segurança web e de APIs

Segurança web e de APIs

OWASP Top 10 explicado para desenvolvedores

O que é o OWASP Top 10, o que cada categoria de 2025 significa, como se defender e como mapear desafios do Juice Shop.

Iniciante15 min de leitura

Segurança web e de APIs

Cross-Site Scripting (XSS) explicado: tipos e defesas

Entenda XSS refletido, armazenado e baseado em DOM, por que a codificação depende do contexto e como a CSP reduz o risco, em laboratório local.

Intermediário11 min de leitura

Segurança web e de APIs

O que é CSRF e como os cookies SameSite ajudam

CSRF usa os cookies do seu próprio navegador contra você. Veja as defesas da mais forte para a mais fraca e teste o SameSite com um laboratório Flask.

Intermediário12 min de leitura