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.
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.
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.
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.
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.
Comece com um modelo que um desenvolvedor poderia escrever:
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:
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:
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 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.
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.
docker run -d -p 127.0.0.1:3000:3000 bkimminich/juice-shopO 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:
curl -s http://127.0.0.1:3000/rest/admin/application-versionNa máquina de teste, a resposta foi:
{"version":"20.3.0"}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:
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:
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:
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.
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.
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.
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:
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.
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:
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.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.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.WHERE é um ponto de concatenação, mesmo quando o resto da aplicação usa o ORM corretamente.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.
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.
Segurança web e de APIs
O que é o OWASP Top 10, o que cada categoria de 2025 significa, como se defender e como mapear desafios do Juice Shop.
Segurança web e de APIs
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.
Segurança web e de APIs
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.