flawopen.com/Injeção de SQL/Python

Injeção de SQL em Python

Crítica CWE-89 Rascunho — aguardando revisão
Language: English Deutsch Español Français हिन्दी Bahasa Indonesia 日本語 한국어 Português (Brasil) Русский 简体中文
Explicação simples

Imagine um formulário que só espera um número de bilhete, como 482. A injeção de SQL acontece quando alguém digita algo malicioso nessa caixa em vez de um número — uma frase armadilhada que faz o sistema responder "mostre todos os bilhetes" em vez de apenas o bilhete 482, porque o sistema nunca verificou se o que recebeu era realmente apenas um número.

Termos-chave desta página
entrada controlada pelo usuário
Qualquer valor que, em última instância, veio de quem está usando — ou atacando — a aplicação: um campo de formulário, um parâmetro de URL, o nome de um arquivo enviado, um cabeçalho HTTP. A aplicação não pode presumir que esse valor é bem formado ou seguro.
consulta SQL
O comando enviado a um banco de dados — por exemplo, "buscar esta linha", "apagar esta tabela". Seu significado vem inteiramente do texto exato, o que é justamente o que torna perigoso injetar texto extra nele.

O que está acontecendo

A injeção de SQL acontece quando entrada controlada pelo usuário é inserida diretamente no texto de uma consulta ao banco de dados, em vez de ser passada como um valor separado. Se o código monta a consulta colando strings, um atacante pode fornecer uma entrada que altera a estrutura real da consulta — transformando uma consulta de "buscar uma linha" em uma que retorna todas as linhas, ou apaga uma tabela.

Em Python, isso quase sempre aparece da mesma forma: uma chamada ao banco de dados construída com f-string, formatação com %, ou concatenação com +, em vez dos placeholders de parâmetros embutidos no driver do banco.

Impacto no mundo real

Em 2015, a operadora britânica TalkTalk sofreu um vazamento que afetou mais de 150 mil clientes depois que atacantes exploraram uma falha de injeção de SQL em uma página web legada herdada de uma aquisição. O órgão regulador de proteção de dados do Reino Unido multou a TalkTalk em £400.000, descrevendo a falha como evitável e básica.

Fonte: notificação de aplicação do Information Commissioner's Office do Reino Unido, 2016 — ver Referências abaixo.

Vulnerável vs. corrigido

VULNERÁVEL
# user_id vem diretamente da requisição
def get_user(cursor, user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"
    cursor.execute(query)
    return cursor.fetchone()
CORRIGIDO
# o valor é passado separadamente, nunca embutido
def get_user(cursor, user_id):
    query = "SELECT * FROM users WHERE id = %s"
    cursor.execute(query, (user_id,))
    return cursor.fetchone()

Por que a correção funciona

A versão corrigida passa o texto da consulta e o valor para execute() como dois argumentos separados. O driver do banco de dados também os envia separadamente ao banco — a estrutura da consulta é fixada antes que o valor seja anexado a ela, então o valor nunca pode ser interpretado como parte da sintaxe SQL, não importa quais caracteres contenha. Uma f-string não consegue fazer isso, porque, no momento em que execute() recebe a consulta, o valor já foi incorporado ao texto como se sempre tivesse feito parte do comando.

Particularidades do Python

Formatação com % dentro de execute() ainda parece parametrizada — mas não é

cursor.execute("... WHERE id = %s" % user_id) é tão vulnerável quanto uma f-string. O placeholder só se torna seguro quando o valor é passado como o próprio segundo argumento de execute()cursor.execute("...WHERE id = %s", (user_id,)) — para que seja o driver, e não a formatação de strings do Python, a fazer a substituição.

ORMs parametrizam por padrão, mas suas válvulas de escape não

O ORM do Django e o query builder do SQLAlchemy parametrizam automaticamente em consultas normais. O risco volta no momento em que você usa Model.objects.raw() ou text() do SQLAlchemy e monta esse SQL bruto com uma f-string.

A sintaxe do placeholder não é consistente entre drivers

psycopg2 (PostgreSQL) usa %s independentemente do tipo da coluna; sqlite3 usa ?. Copiar o estilo de placeholder da documentação de um driver para outro quebra silenciosamente — verifique o estilo de parâmetro do seu driver específico em vez de presumir.

Equívocos comuns

"Meu ORM me protege automaticamente"

Verdadeiro para a API de consulta normal do ORM — falso no momento em que você usa raw() ou text() e monta essa string você mesmo.

"Esse ID é sempre um número, então é seguro interpolar"

O risco não é o tipo do valor em tempo de execução — é o fato de a consulta ser construída por interpolação de string. No momento em que essa suposição falhar em qualquer ponto do ciclo de vida do código, a vulnerabilidade já estará lá esperando.

"Eu mesmo faço o escape das aspas, então não preciso de consultas parametrizadas"

O escape manual é específico do driver e fácil de errar de forma sutil. Consultas parametrizadas não são uma versão mais rigorosa do escape — elas evitam o problema por completo, porque o valor nunca faz parte do texto da consulta.

Como verificar se você está afetado

grep -rn "execute(f\"" --include="*.py" . grep -rn "execute(.*%\s*(" --include="*.py" . grep -rn "\.raw(\|text(" --include="*.py" .
Melhor do que apenas grep: rode o Bandit (regra B608, hardcoded_sql_expressions) no CI — ele detecta esse padrão automaticamente e falha o build em uma nova ocorrência.

Checklist de prevenção

Perguntas frequentes

Usar um ORM me protege de injeção de SQL?

Para seus métodos de consulta normais, sim. As válvulas de escape para consultas brutas não protegem — elas são exatamente tão seguras quanto SQL escrito à mão, nem mais.

Esse risco existe só em coisas como campos de busca?

Não. Qualquer coisa efetivamente controlada por terceiros conta — cabeçalhos HTTP, nomes de arquivos enviados, até um valor vindo de uma API de terceiros em que sua aplicação confia.

Posso simplesmente escapar as aspas eu mesmo?

Você pode, mas é frágil e específico do driver. Consultas parametrizadas são a correção de fato, não uma versão mais rigorosa do escape.

Referências

Ver em: Python JavaScriptGoJava PHPC#Ruby C/C++RustKotlin Swift Solidity (N/A)
Veja também: Injeção de ComandoPath Traversal XSSDesserialização Insegura