SQL Injection

Até aqui, quem escrevia o SQL era você. Num site ou num aplicativo, quem escreve é um programa, e muitas vezes ele junta um pedaço fixo de SQL com o que a pessoa digitou num formulário. Se essa junção for feita do jeito ingênuo, quem digita pode escrever SQL no lugar do dado e mudar a consulta: ver a tabela inteira de clientes, entrar sem senha, ler os salários dos funcionários. Esse problema se chama SQL Injection ("injeção de SQL") e é uma das falhas mais antigas e mais comuns da web. Nesta aula você vai ver o problema acontecer de verdade, no editor e num programa Python da Ana, para aprender a se proteger dele.

Você vai aprender
  • Como um programa "monta" uma consulta colando texto, e por que isso é perigoso.
  • O caso clássico ' OR '1'='1, que faz a tabela inteira aparecer.
  • Como o mesmo descuido deixa alguém entrar sem senha e ler outra tabela com UNION.
  • Por que algumas bibliotecas rodam um comando só, e por que isso não é a sua proteção.
  • As defesas, em ordem: parâmetros (a principal), lista de valores permitidos e usuário com poucas permissões.
  • O sinal de alarme: o cliente honesto com apóstrofo no nome que "quebra" o sistema.
Para que serve esta aula

Esta é uma aula de defesa. Você vê o problema para entender por que ele acontece e para reconhecer código frágil no seu próprio trabalho. Pratique só no editor do guia, no seu próprio computador ou em sistemas em que você tem autorização por escrito. Mexer no sistema dos outros sem permissão é crime (no Brasil, a Lei 12.737/2012 trata da invasão de dispositivo informático).

Os exemplos em SQL rodam no editor do guia, contra o banco da Loja Aurora. Cada Rodar parte do banco limpo, então você pode testar à vontade: nada se perde. Os programas em Python rodaram de verdade na máquina da Ana, no banco ~/loja.db.


A ideia: o bilhete preenchido pelo cliente

Imagine que, na Loja Aurora, o atendente usa um bloco de bilhetes já impressos para pedir uma ficha ao arquivo:

Buscar no arquivo a ficha do cliente com este email: ____________

Alguém preenche a linha em branco e o arquivista faz exatamente o que está escrito no bilhete, sem pensar. Um cliente honesto escreve o próprio email. Um espertinho escreve:

Buscar no arquivo a ficha do cliente com este email: ninguém; e, já que veio aqui, traga TODAS as fichas

O arquivista lê o bilhete inteiro como uma ordem só e traz o arquivo todo. O problema não é o arquivista, que só obedece. O problema é que o dado (o que foi escrito na linha) e a ordem (o texto impresso) foram parar no mesmo papel, sem nada que separe um do outro. No SQL Injection é igual: o banco obedece ao texto que recebe, e o programa deixou o visitante escrever um pedaço desse texto.


1. Como um programa monta uma consulta

A Ana fez um programinha em Python que procura um cliente pelo email. Ele pergunta o email, cola o que foi digitado no meio do SQL e manda para o banco:

import sqlite3

con = sqlite3.connect("loja.db")
email = input("Email do cliente: ")

sql = "SELECT nome, email FROM clientes WHERE email = '" + email + "'"
print("SQL montado:", sql)

linhas = con.execute(sql).fetchall()
for linha in linhas:
    print(linha)
print(len(linhas), "linha(s)")
input("Email do cliente: ")
Mostra a pergunta e espera a pessoa digitar. O que ela digitar vai para a variável email.
"…WHERE email = '" + email + "'"
A linha frágil. Ela junta (com o +) três pedaços de texto: o começo do SQL até a aspa que abre, o que a pessoa digitou e a aspa que fecha.
print("SQL montado:", sql)
Mostra o SQL que vai para o banco. Programas de verdade não fazem isso; está aqui só para você ver a montagem.
con.execute(sql)
Manda o texto pronto para o SQLite rodar. O banco não sabe que parte veio da Ana e que parte veio do visitante: para ele, é tudo SQL.

Com um email normal, tudo funciona:

$ python3 busca.py
Email do cliente: ana.beatriz@exemplo.com.br      « a Ana digita o email e aperta Enter
SQL montado: SELECT nome, email FROM clientes WHERE email = 'ana.beatriz@exemplo.com.br'
('Ana Beatriz Sousa', 'ana.beatriz@exemplo.com.br')
1 linha(s)

É a mesma consulta que você escreveria no editor:

SELECT nome, email FROM clientes WHERE email = 'ana.beatriz@exemplo.com.br';

2. Quando o texto digitado vira SQL: ' OR '1'='1

Agora alguém digita, no lugar do email, este texto: ' OR '1'='1. O programa cola tudo do mesmo jeito, sem desconfiar:

$ python3 busca.py
Email do cliente: ' OR '1'='1
SQL montado: SELECT nome, email FROM clientes WHERE email = '' OR '1'='1'
('José Ribamar Silva', 'jose.ribamar@exemplo.com.br')
('Maria Clara Mendes', 'maria.clara@exemplo.com.br')
('Ana Beatriz Sousa', 'ana.beatriz@exemplo.com.br')
('Carlos Eduardo Lima', 'carlos.lima@exemplo.com.br')
('Conceição Prado', 'conceicao.prado@exemplo.com.br')
('Rafael Monteiro', 'rafael.monteiro@exemplo.com.br')
('Luís Otávio Brito', None)
('Fernanda Carvalho', 'fernanda.carvalho@exemplo.com.br')
('Antônio Carlos Neves', 'antonio.neves@exemplo.com.br')
('Juliana Paes Barbosa', 'juliana.barbosa@exemplo.com.br')
('Marcos Vinícius Rocha', 'marcos.rocha@exemplo.com.br')
('Patrícia Gusmão', 'patricia.gusmao@exemplo.com.br')
('Tiago Fontes', 'tiago.fontes@exemplo.com.br')
('Camila Duarte', 'camila.duarte@exemplo.com.br')
('Roberto Falcão', None)
('Larissa Macêdo', 'larissa.macedo@exemplo.com.br')
('Gustavo Henrique Prado', 'gustavo.prado@exemplo.com.br')
('Ana Carvalho Lima', 'ana.carvalho@exemplo.com.br')
('Bruno Cavalcante', 'bruno.cavalcante@exemplo.com.br')
('Vitória Régia Campos', 'vitoria.campos@exemplo.com.br')
('Sebastião Moura', 'sebastiao.moura@exemplo.com.br')
('Débora Cristina Alves', 'debora.alves@exemplo.com.br')
('Henrique Sales', None)
('Isabela Franco', 'isabela.franco@exemplo.com.br')
('Otávio Bezerra', 'otavio.bezerra@exemplo.com.br')
('Mirela Pontes', 'mirela.pontes@exemplo.com.br')
('Paulo Sérgio Andrade', 'paulo.andrade@exemplo.com.br')
('Tainá Soares', 'taina.soares@exemplo.com.br')
('Elias Furtado', 'elias.furtado@exemplo.com.br')
('Rosângela Dias', 'rosangela.dias@exemplo.com.br')
30 linha(s)          « os 30 clientes da loja, com o email de todos

O programa devia mostrar um cliente e mostrou os 30. Rode a consulta montada no editor e veja a tabela inteira aparecer:

SELECT nome, email FROM clientes WHERE email = '' OR '1'='1';

O que mudou está nas aspas. Olhe o SQL montado com o que o visitante digitou marcado entre colchetes:

SELECT nome, email FROM clientes WHERE email = '[' OR '1'='1]'
                                                ↑  └── digitado ──┘↑
                                       aspa do programa      aspa do programa
email = ''
A primeira aspa que a pessoa digitou fecha o texto que o programa tinha aberto. Para o banco, o email procurado é o texto vazio.
OR '1'='1'
Depois do texto vazio sobra isto, que o banco agora lê como SQL, não como dado: "ou então, quando '1' for igual a '1'". E '1' é sempre igual a '1'.
a última aspa
A aspa que o programa colou no fim casa com a última aspa digitada, então o SQL fica bem-formado e o banco não reclama.

Como a condição OR '1'='1' é verdadeira em toda linha, o WHERE deixa passar a tabela inteira. Foi o "traga todas as fichas" do bilhete. Repare: quem digitou nem precisava saber um email de verdade.

Experimente: a caixa está com a consulta alterada, mostrando os 30 clientes. Conserte o WHERE para que ela volte a procurar um email de verdade (o da Ana Beatriz, ana.beatriz@exemplo.com.br) e devolva só 1 linha. É assim que o programa deveria ter se comportado.

SELECT nome, email FROM clientes WHERE email = '' OR '1'='1';
SELECT nome, email FROM clientes WHERE email = 'ana.beatriz@exemplo.com.br';

3. Entrar sem senha, com um comentário

O estrago cresce quando a consulta decide quem entra num sistema. Imagine uma tabela de logins (vamos criá-la aqui só para a demonstração) e um programa que confere login e senha montando o SQL por colagem:

SELECT login FROM usuarios
WHERE login = 'DIGITADO' AND senha = 'DIGITADO';

Se vier uma linha, o programa deixa entrar. Veja o que acontece quando o invasor digita, no campo do login, caio'-- e deixa a senha em branco. As duas aspas e os dois tracinhos fazem o serviço:

CREATE TABLE usuarios (login TEXT, senha TEXT);
INSERT INTO usuarios VALUES ('ana', 'girassol42'), ('caio', 'teresina2024');

SELECT login FROM usuarios
WHERE login = 'caio'--' AND senha = 'qualquer';
login = 'caio'
A aspa digitada depois de caio fecha o texto. O banco entende: login igual a caio.
--
Em SQL, -- começa um comentário: daí até o fim da linha, o banco ignora tudo (veja a aula Comentários). Some a parte AND senha = '…'.
o resto da linha
Fica dentro do comentário, então a checagem da senha nunca acontece. A consulta devolve a linha do caio, e o programa deixa entrar sem conferir a senha.

Rodou e devolveu a linha caio: para o programa, login aprovado. O invasor entrou na conta de alguém só sabendo (ou chutando) o nome de login.

E se a pessoa nem souber um login?

Aí entra o OR da seção 2, agora do lado do login: digitando ' OR '1'='1 no campo de login, o WHERE fica verdadeiro para todas as linhas e a consulta devolve a tabela inteira de usuários. O programa, que só olha se "veio alguma linha", deixa entrar. É o mesmo descuido da busca de clientes, com uma consequência bem pior.


4. Ler outra tabela com UNION

Lembra do UNION (aula UNION), que empilha o resultado de duas consultas? Se a consulta frágil mostra uma lista na tela, quem ataca pode pendurar um SELECT de outra tabela no fim e fazer esses dados aparecerem no lugar da lista.

A loja tem uma busca de produtos pelo nome. O invasor digita um texto que fecha o LIKE, acrescenta um UNION SELECT dos salários dos funcionários e comenta o resto. Como os salários não são para o público, isso é um vazamento:

SELECT nome, preco FROM produtos WHERE nome LIKE '%zzz' UNION SELECT nome, salario FROM funcionarios --%';
LIKE '%zzz'
Nenhum produto casa com %zzz, então a primeira consulta vem vazia. O invasor fez de propósito: ele quer só a segunda metade.
UNION SELECT nome, salario FROM funcionarios
Empilha os salários. Para o UNION funcionar, as duas consultas precisam do mesmo número de colunas; por isso o invasor escolheu duas (nome e salario), casando com nome, preco.
--%'
O -- comenta o pedaço %' que o programa ia colar no fim, para o SQL não ficar quebrado.

No lugar dos produtos, apareceu a folha de pagamento. Em sistemas reais, esse mesmo truque já serviu para ler tabelas de senhas e de cartões. O invasor costuma descobrir quantas colunas usar testando aos poucos; os detalhes não importam aqui — o que importa é a causa, sempre a mesma: texto digitado virou SQL.


5. "Mas a minha biblioteca só roda um comando" — e os dois tracinhos

Você pode ter ouvido que o perigo do SQL Injection é alguém mandar um segundo comando junto, separado por ;, como um DELETE ou um DROP TABLE grudado na consulta. Isso existe, mas muitas bibliotecas recusam dois comandos numa chamada só. O sqlite3 do Python é uma delas. Quando a Ana manda, pela colagem, um texto com ; no meio, o próprio Python barra:

$ echo "'; DELETE FROM clientes; --" | python3 busca.py
Email do cliente: SQL montado: SELECT nome, email FROM clientes WHERE email = ''; DELETE FROM clientes; --'
Traceback (most recent call last):
  File "/home/ana/busca.py", line 9, in <module>
    linhas = con.execute(sql).fetchall()
             ~~~~~~~~~~~^^^^^
sqlite3.ProgrammingError: You can only execute one statement at a time.

A mensagem diz, em inglês: "você só pode executar um comando por vez". O método execute() do Python compila e roda só o primeiro comando do texto. Isso é parecido com o que a biblioteca C do SQLite faz por baixo: a documentação oficial da função sqlite3_prepare_v2 diz que ela "só compila o primeiro comando" do texto e deixa o resto de lado.

Isso protege você? Não.

Veja de novo as seções 2, 3 e 4: nenhum daqueles ataques precisou de um segundo comando. O ' OR '1'='1, o login sem senha e o UNION são um comando só — a própria consulta, alterada por dentro. Contar com "a biblioteca só roda um comando" é uma falsa sensação de segurança. E há bibliotecas e funções que aceitam vários comandos (no PHP, por exemplo, a mysqli_multi_query(), cuja própria documentação avisa para usar prepared statements quando houver dado vindo de fora). A única defesa de verdade é a da próxima seção.

E por que o -- dos ataques some com o resto da linha? Porque, como você viu na aula Comentários, -- começa um comentário até o fim da linha. O invasor usa isso para "cortar fora" o pedaço de SQL que o programa ia colar depois do dado dele.

Nos outros bancos: o comentário tem variações

O comentário -- funciona em MySQL, PostgreSQL, SQL Server e SQLite. Há uma pegadinha: no MySQL, a documentação oficial exige que, depois dos dois tracinhos, venha um espaço (ou tab) — -- — senão não é comentário. O MySQL também aceita # como comentário até o fim da linha, o que o SQLite não aceita. Nada disso muda o remédio: parâmetros.


6. A defesa de verdade: parâmetros

A causa de tudo é sempre a mesma: o dado e o SQL andaram juntos no mesmo texto. A solução é separá-los. Em vez de colar o valor dentro do SQL, você deixa um espaço reservado (um ?) e entrega o valor por fora, numa lista à parte. O banco monta a consulta primeiro e só depois encaixa o valor, tratando-o sempre como dado, nunca como comando. Isso se chama consulta com parâmetros (ou prepared statement), e tem aula só para ela: Consultas com Parâmetros.

Veja o mesmo programa da Ana, agora com ? no lugar da colagem:

import sqlite3

con = sqlite3.connect("loja.db")
email = input("Email do cliente: ")

sql = "SELECT nome, email FROM clientes WHERE email = ?"
print("SQL enviado:", sql)

linhas = con.execute(sql, (email,)).fetchall()
for linha in linhas:
    print(linha)
print(len(linhas), "linha(s)")
WHERE email = ?
O ? é o espaço reservado. O SQL agora é fixo: não muda com o que a pessoa digita. Repare que não há aspa nenhuma em volta do ?.
con.execute(sql, (email,))
O valor vai no segundo argumento, numa lista à parte. O banco recebe a consulta e o valor por caminhos separados e encaixa o valor como dado.

Agora o ataque não cola mais. O mesmo ' OR '1'='1 de antes é tratado como um email literal — e, como nenhum cliente tem esse email esquisito, a busca volta vazia:

$ echo "ana.beatriz@exemplo.com.br" | python3 busca_segura.py
Email do cliente: SQL enviado: SELECT nome, email FROM clientes WHERE email = ?
('Ana Beatriz Sousa', 'ana.beatriz@exemplo.com.br')
1 linha(s)
$ echo "' OR '1'='1" | python3 busca_segura.py
Email do cliente: SQL enviado: SELECT nome, email FROM clientes WHERE email = ?
0 linha(s)          « o texto do ataque virou um email comum: ninguém tem esse email
$ echo "'; DELETE FROM clientes; --" | python3 busca_segura.py
Email do cliente: SQL enviado: SELECT nome, email FROM clientes WHERE email = ?
0 linha(s)          « idem: procurou por um "email" bem esquisito, não achou

Repare que o SQL enviado é sempre o mesmo, …email = ?. O que a pessoa digita nunca mais vira comando. Essa é a regra de ouro: todo valor que vem de fora entra por parâmetro.


7. O sinal de alarme honesto: o apóstrofo no nome

Você não precisa de um invasor para descobrir que o seu código cola texto. Basta um cliente de verdade com apóstrofo no nome. A Loja Aurora tem um fornecedor assim: Loja D'Água. Procure por ele com a busca que cola texto:

SELECT id, nome FROM fornecedores WHERE nome = 'Loja D'Água';

O banco respondeu com um erro (near "Água": syntax error — "erro de sintaxe perto de Água"). O apóstrofo de D'Água fechou o texto cedo demais, exatamente como a aspa do ataque, e o Água' que sobrou virou SQL sem sentido. Veja o que o banco recebeu:

WHERE nome = 'Loja D'Água'
                   ↑ a aspa aqui fechou o texto; sobrou  Água'

Esse erro é um presente: ele avisa, com um cliente honesto, que o seu programa cola texto — e o que hoje é um erro chato, amanhã é a porta do ataque. Com parâmetros, o problema simplesmente não existe: o nome com apóstrofo é guardado e procurado certinho, porque o banco o trata como dado. A aula Consultas com Parâmetros mostra isso rodando. (E, quando você precisa escrever a aspa dentro do SQL à mão, ela é dobrada: 'Loja D''Água', como na aula WHERE.)


8. As defesas, em ordem

Parâmetros resolvem quase tudo. As outras defesas são reforços, para os casos em que um valor não pode entrar por parâmetro.

DefesaQuando usarO que faz
Parâmetros (?, :nome)Sempre, para todo valor que vem de foraSepara o dado do SQL; o dado nunca vira comando. É a defesa principal.
Lista de permitidosQuando o que varia é um nome de coluna ou a direção do ORDER BY (que não dá para passar por ?)Você compara o que veio com uma lista fixa que você escreveu ('preco', 'nome'…) e recusa o resto. Nunca cole um nome de coluna vindo de fora.
Usuário com poucas permissõesEm bancos com servidor (MySQL, PostgreSQL…)Se o site só lê, o usuário do banco só tem permissão de SELECT. Mesmo que algo escape, o estrago é menor. Veja Usuários e Permissões.
Escapar o texto à mãoÚltimo recurso, quando não dá para usar parâmetroDobrar as aspas do valor você mesmo. É fácil errar; prefira sempre o parâmetro.
Validar não é o mesmo que proteger

"Só aceitar emails com @" ou "proibir a palavra DROP" ajuda a melhorar o formulário, mas não protege contra SQL Injection: o ' OR '1'='1 não tem @ nem a palavra DROP, e listas de palavras proibidas sempre deixam um buraco. A proteção vem da separação entre dado e SQL, ou seja, dos parâmetros. Validação é um acréscimo, não a defesa.


Quando dá errado (no editor)

A aspa dobrada dentro do texto

No editor, se você quiser procurar o fornecedor com apóstrofo, a forma certa é dobrar a aspa dentro do texto — e aí funciona:

SELECT id, nome FROM fornecedores WHERE nome = 'Loja D''Água';

As duas aspas seguidas ('') dizem ao banco "aqui é uma aspa de verdade, parte do texto", e o texto não é fechado cedo. É exatamente esse cuidado que os parâmetros fazem por você, sem você precisar lembrar.

As aspas duplas não resolvem

Pode parecer que trocar ' por " em volta do texto resolveria. No editor do guia, aspas duplas são para nomes de coluna, não para texto. Procurar um email assim dá erro de coluna:

SELECT nome FROM clientes WHERE email = "ana.beatriz@exemplo.com.br";

A mensagem é no such column: "ana.beatriz@exemplo.com.br" - should this be a string literal in single-quotes? ("não existe essa coluna… isto não deveria ser um texto entre aspas simples?"): o banco achou que você citou uma coluna com esse nome enorme e ainda sugeriu a correção. Texto vai sempre entre aspas simples — e, melhor ainda, por parâmetro.


Referência rápida

TermoO que é
SQL InjectionQuando texto digitado por um visitante vira parte do SQL e muda a consulta.
' OR '1'='1Texto que fecha a aspa e acrescenta uma condição sempre verdadeira: devolve a tabela inteira.
'--Fecha a aspa e comenta o resto da linha, cortando fora a checagem de senha ou o fim da consulta.
UNION SELECT … injetadoPendura outra consulta no fim para ler uma tabela que não era para aparecer.
Parâmetro (?, :nome)Espaço reservado; o valor vai por fora e é sempre tratado como dado. A defesa principal.
Lista de permitidosPara nome de coluna ou direção de ORDER BY, que não cabem num parâmetro.

Exercícios

1. Por que o texto ' OR '1'='1 faz a consulta devolver a tabela inteira?

2. A biblioteca sqlite3 do Python recusa rodar dois comandos de uma vez. Isso basta para proteger contra SQL Injection?

3. Qual é a defesa principal contra SQL Injection?

4. O comentário que corta a senha

No ataque de login, o invasor digitou caio'--. Complete os dois caracteres que começam o comentário que faz o banco ignorar o resto da linha:

login = 'caio' ' AND senha = ...

Missão final: transforme o ataque numa busca honesta

Missão: a caixa tem uma busca de fornecedor alterada para devolver todos os 6. Reescreva o WHERE para procurar só o fornecedor Loja D'Água (atenção ao apóstrofo: dentro do texto, dobre a aspa) e devolver 1 linha. É o que um parâmetro faria por você sem esse trabalho todo.

SELECT id, nome FROM fornecedores WHERE nome = '' OR '1'='1';
SELECT id, nome FROM fornecedores WHERE nome = 'Loja D''Água';

Resumo

  • SQL Injection acontece quando um programa cola o texto digitado por alguém dentro do SQL: o dado pode virar comando e mudar a consulta.
  • ' OR '1'='1 fecha o texto e acrescenta uma condição sempre verdadeira, devolvendo a tabela inteira. Um '-- comenta o resto e pula a checagem de senha. Um UNION SELECT injetado lê outra tabela.
  • "A minha biblioteca só roda um comando" não é defesa: os piores ataques usam um comando só.
  • A defesa de verdade é parâmetro (?, :nome): o valor vai por fora e é sempre tratado como dado. Para nome de coluna, use uma lista de permitidos. Em bancos com servidor, dê poucas permissões ao usuário.
  • Um cliente honesto com apóstrofo no nome (Loja D'Água) já quebra o código que cola texto — é o aviso de que falta parâmetro.
  • A aula Consultas com Parâmetros mostra os parâmetros em detalhe.