Menu

SQL Injection no SQLite: como prevenir com queries parametrizadas

Por que concatenar strings em SQL é furada, como um ataque de SQL injection funciona na prática e como queries parametrizadas no SQLite resolvem o problema de vez.

Esta página tem editores executáveis — edite, execute e veja a saída na hora.

SQL Injection É um Bug de Concatenação de Strings

SQL injection acontece quando a entrada do usuário acaba virando parte do texto SQL que o banco interpreta. No momento em que essa fronteira se rompe — quando aquilo que o usuário digitou vira sintaxe executada pelo banco — ele passa a ter o mesmo poder que você tem.

Olha o anti-padrão clássico, em pseudocódigo que qualquer linguagem consegue reproduzir:

-- NÃO FAÇA ISSO
query = "SELECT * FROM users WHERE name = '" + user_input + "'"

Se user_input for Ada, a consulta funciona normalmente. Agora, se user_input for ' OR 1=1 --, o que acontece é o seguinte:

SELECT * FROM users WHERE name = '' OR 1=1 --'

O -- comenta a aspa que sobrou, o OR 1=1 casa com todas as linhas e pronto: o atacante acabou de vazar sua tabela de usuários inteira. Versões piores ainda encadeiam ; com uma segunda instrução para dropar tabelas, exfiltrar dados ou inserir uma nova conta de admin.

A vulnerabilidade não está no SQLite. Está no código que montou aquela string.

Query parametrizada: a solução de verdade

Uma query parametrizada separa o texto SQL dos valores. O SQL fica com placeholders — ? ou :nome — e você passa os valores à parte. O SQLite faz o parse e compila o SQL uma vez, e depois faz o bind dos seus valores no plano já compilado. Os valores nunca viram SQL.

Veja como rodar aquela busca aparentemente vulnerável de forma segura:

No shell do SQLite você digita o valor direto, mas no código da sua aplicação o equivalente fica assim (usando o driver sqlite3 do Python):

# Python — parametrizado, seguro
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))

Passe a SQL e a tupla de valores como dois argumentos separados. O driver envia cada coisa para o SQLite de forma independente. Mesmo que user_input seja ' OR 1=1 --, o SQLite vai procurar um usuário cujo nome seja literalmente ' OR 1=1 -- — e, claro, não encontra ninguém.

O que significa "seguro" de verdade aqui

A segurança não vem de comparar padrões nem de escapar aspas. Ela é estrutural. O SQLite compila o comando numa representação interna antes mesmo de enxergar o seu valor:

-- A instrução compilada tem um slot, não uma string.
SELECT * FROM users WHERE name = ?
                                 ^
                                 slot do placeholder

Quando você faz o bind de um valor, ele entra naquele slot já como um dado tipado — TEXT, INTEGER, BLOB, o que for. O SQLite nunca volta a interpretar aquilo como SQL. Não existe sintaxe que o atacante possa injetar, porque o parser já terminou o trabalho dele.

É por isso que query parametrizada é confiável de um jeito que escapar aspas nunca vai ser. Escapar tenta limpar caracteres perigosos de uma string. O bind de parâmetros simplesmente nunca chega a montar a string perigosa.

Fuja da formatação de strings

Toda linguagem tem aquele atalho tentador — f-strings no Python, template literals no JavaScript, String.format em Java — e todos eles são uma cilada quando o assunto é SQL.

# NÃO FAÇA — f-string interpola o valor diretamente no texto SQL
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")

# NÃO FAÇA — mesmo problema, formatação com %
cursor.execute("SELECT * FROM users WHERE name = '%s'" % user_input)

# FAÇA — placeholder + argumento de valores
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))

Os dois primeiros jogam a entrada do usuário direto na string SQL antes mesmo do driver ver. Quando o SQLite recebe a query, o estrago já está feito. O terceiro mantém o SQL e o valor em trilhos separados.

A regra é mecânica: se você se pegar montando uma string SQL com +, f-strings, format ou template literals bem no lugar onde entra um valor — pare e use um placeholder.

Múltiplos parâmetros e placeholders nomeados

Na prática, a maioria das queries tem mais de um valor. O SQLite aceita tanto placeholders posicionais ? quanto nomeados :nome:

No código da aplicação, isso se traduz em:

# Posicional
cursor.execute(
    "SELECT * FROM orders WHERE customer = ? AND status = ?",
    ("Ada", "paid"),
)

# Nomeado — mais claro quando há vários parâmetros
cursor.execute(
    "SELECT * FROM orders WHERE total > :min_total AND status = :status",
    {"min_total": 50, "status": "paid"},
)

Parâmetros nomeados escalam melhor. Quando você passa de três ou quatro valores, ?, ?, ?, ? vira um joguinho de adivinhação; já :customer, :total, :status, :created_at se documenta sozinho.

Identificadores pedem outra abordagem

Os parâmetros vinculados (bind parameters) só funcionam para valores — aquilo que aparece do lado direito de um =, dentro de IN (...) ou em VALUES (...). Eles não servem para nomes de tabelas, nomes de colunas nem palavras-chave do SQL como ASC/DESC.

-- Isto NÃO funciona. O placeholder não pode substituir o nome de uma coluna.
SELECT * FROM users ORDER BY ? ASC

Se você precisar de um identificador dinâmico — por exemplo, deixar o usuário escolher por qual coluna ordenar — valide contra uma lista de permissões antes de montar o SQL:

# Abordagem de lista de permitidos
ALLOWED_SORT_COLUMNS = {"name", "created_at", "role"}

if sort_column not in ALLOWED_SORT_COLUMNS:
    raise ValueError(f"Coluna de ordenação inválida: {sort_column}")

query = f"SELECT * FROM users ORDER BY {sort_column} ASC"
cursor.execute(query)

A string vinda do usuário é validada contra um conjunto fixo de valores seguros antes de chegar perto do SQL. A f-string só é aceitável aqui porque sort_column não pode mais ser nada além de um dos três nomes fixos no código.

Exemplo de SQL injection neutralizado na prática

Vamos comparar as duas versões lado a lado, usando uma entrada maliciosa. Primeiro, monte uma tabela users simples:

O formulário vulnerável retorna todos os usuários. Já a versão parametrizada procura literalmente por um usuário chamado ' OR 1=1 -- e não retorna nada. Mesma entrada, resultados completamente diferentes — porque, no segundo caso, o valor nunca virou SQL.

Checklist rápido para prevenir SQL injection

  • Use placeholders ? ou :nome para todo valor que vem de fora do seu código — input do usuário, corpo de requisição, variáveis de ambiente, qualquer coisa que você não escreveu inline.
  • Nunca monte SQL com +, f-strings ou format no lugar onde entra um valor.
  • Para nomes dinâmicos de tabela ou coluna, valide contra uma allowlist fixa antes de injetar na query.
  • Confie no driver. Não escreva sua própria função de escape de aspas. O mecanismo de bind parameters é mais antigo, mais testado em produção e está correto.
  • Revise as queries do seu time com uma única pergunta na cabeça: tem algum input do usuário sendo concatenado dentro do texto SQL? Se sim, corrija.

Quando esse hábito vira reflexo, SQL injection deixa de ser uma classe de bug com a qual você precisa se preocupar.

Próximo passo: conectando a partir de aplicações

Você já viu o formato seguro de uma query — placeholder no SQL, valor passado em separado. A próxima página mostra como conectar o SQLite a partir de código de aplicação real em Python, Node.js e outras linguagens, incluindo gerenciamento de conexões e onde as queries parametrizadas se encaixam num fluxo típico de requisição.

Perguntas frequentes

O SQLite é vulnerável a SQL injection?

É sim. O SQLite é tão vulnerável quanto qualquer outro banco SQL quando a aplicação monta queries concatenando strings. E a solução não está em alguma configuração do SQLite — está em como você passa os valores no código. Use queries parametrizadas com placeholders ? ou :nome e deixe o driver fazer o trabalho com segurança.

Como queries parametrizadas previnem SQL injection?

Quando você usa placeholders como ?, o SQLite primeiro analisa e compila a query, e só depois encaixa seus valores nos slots da instrução já compilada. Os valores nunca chegam a virar sintaxe SQL — eles são tratados como dado puro, ponto. Não existe string para o atacante 'escapar'.

Não dá pra simplesmente escapar as aspas da entrada do usuário?

Não dá. Escapar manualmente é frágil — uma hora você esquece um caso (aspas Unicode, truques de encoding, marcadores de comentário) e abre uma vulnerabilidade. Os drivers oferecem ? e :nome justamente pra você não precisar pensar em escape. Use sempre, inclusive em valores que você 'tem certeza' que são seguros.

E quando o nome da tabela ou da coluna vem do usuário?

Parâmetros vinculados só funcionam para valores, não para identificadores. Se o nome de uma tabela ou coluna precisa ser dinâmico, valide contra uma allowlist de nomes conhecidos antes de juntar na SQL. Nunca jogue um identificador vindo direto do usuário em uma string formatada.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR