A segurança de consultas SQL é um dos pilares da proteção de aplicações web. A vulnerabilidade mais conhecida nesse contexto é a injeção de SQL, que ocorre quando dados fornecidos pelo usuário são concatenados diretamente em comandos SQL, permitindo que um atacante manipule a consulta para acessar, alterar ou excluir dados indevidos. Nesta aula, vamos explorar as principais técnicas para mitigar esse risco: prepared statements, uso de ORMs, princípio do menor privilégio e escapes.

Essas técnicas não são mutuamente exclusivas e, em um projeto real, é comum combiná-las para criar camadas de defesa. Compreender cada uma é essencial para escrever código seguro e robusto, independentemente da linguagem de programação ou banco de dados utilizado.

Prepared statements

Prepared statements (ou declarações preparadas) são uma das formas mais eficazes de prevenir injeção de SQL. A ideia central é separar a estrutura do comando SQL dos dados fornecidos pelo usuário. Em vez de montar uma string SQL completa com os valores inseridos diretamente, você define um modelo com placeholders (geralmente representados por ? ou :nome) e, em seguida, envia os valores separadamente. O banco de dados, então, compila o comando SQL apenas uma vez e, nas execuções seguintes, utiliza os parâmetros de forma segura, tratando-os como dados e não como parte do código SQL.

Vamos ver um exemplo em Python usando a biblioteca sqlite3 (que suporta prepared statements). Considere uma consulta que busca um usuário pelo nome:

import sqlite3

conexao = sqlite3.connect("banco.db")
cursor = conexao.cursor()

# Exemplo VULNERÁVEL - NÃO FAÇA ISSO
nome = input("Digite o nome do usuário: ")
cursor.execute(f"SELECT * FROM usuarios WHERE nome = '{nome}'")

# Exemplo SEGURO usando prepared statement
nome = input("Digite o nome do usuário: ")
cursor.execute("SELECT * FROM usuarios WHERE nome = ?", (nome,))

# Exemplo com múltiplos parâmetros
idade = 25
cursor.execute("SELECT * FROM usuarios WHERE nome = ? AND idade > ?", (nome, idade))

conexao.close()

No primeiro caso, se o usuário digitar algo como ' OR '1'='1, a consulta se torna SELECT * FROM usuarios WHERE nome = '' OR '1'='1', retornando todos os registros. No segundo caso, o valor é passado como parâmetro, e o banco de dados o interpreta literalmente, sem qualquer interpretação como código SQL.

Em outras linguagens, como PHP com PDO, a sintaxe é semelhante:

$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE nome = :nome");
$stmt->execute([':nome' => $nome]);

Prepared statements são suportados pela maioria dos bancos de dados relacionais (MySQL, PostgreSQL, SQL Server, etc.) e devem ser a primeira escolha sempre que você precisar executar consultas com dados dinâmicos.

ORM

Um ORM (Object-Relational Mapping) é uma biblioteca que mapeia tabelas do banco de dados para objetos na linguagem de programação, permitindo que você interaja com o banco usando a sintaxe da própria linguagem, sem escrever SQL manualmente. Além de aumentar a produtividade, os ORMs geralmente utilizam prepared statements internamente, o que já oferece uma proteção contra injeção de SQL. No entanto, é importante entender como usá-los corretamente, pois alguns ORMs permitem consultas brutas (raw queries) que podem ser inseguras se não forem parametrizadas.

Vejamos um exemplo com SQLAlchemy (Python) e Eloquent (PHP):

# Exemplo com SQLAlchemy (Python)
from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///banco.db")
with engine.connect() as conn:
    # Uso seguro: parâmetros são passados separadamente
    resultado = conn.execute(text("SELECT * FROM usuarios WHERE nome = :nome"), {"nome": nome})

# Exemplo com Eloquent (Laravel/PHP)
$usuarios = DB::table('usuarios')->where('nome', $nome)->get();

No SQLAlchemy, o método text() permite consultas SQL cruas, mas você deve sempre usar os parâmetros nomeados para evitar injeção. Já o Eloquent constrói a consulta de forma programática, e os valores são tratados como parâmetros automaticamente.

Uma das vantagens dos ORMs é que eles abstraem a complexidade do SQL, mas é preciso tomar cuidado com funcionalidades como query builder (construtor de consultas) que podem permitir a concatenação de strings se você não usar os métodos adequados. Sempre prefira os métodos que aceitam parâmetros vinculados.

Least privilege

O princípio do menor privilégio (least privilege) estabelece que cada componente do sistema (usuário, processo, aplicação) deve ter apenas as permissões estritamente necessárias para realizar suas funções. No contexto de segurança de SQL, isso significa que a conta de banco de dados usada pela aplicação não deve ser um superusuário (como root ou sa), mas sim um usuário com permissões limitadas: somente os privilégios de SELECT, INSERT, UPDATE e DELETE nas tabelas necessárias, e talvez nenhum privilégio de CREATE ou DROP.

Se a aplicação for comprometida, o impacto de uma injeção de SQL será limitado ao que o usuário pode fazer. Por exemplo, se a aplicação só precisa ler dados, o usuário do banco não deve ter permissão de DELETE. Assim, mesmo que um atacante consiga executar uma consulta maliciosa, ele não conseguirá apagar tabelas.

Vamos ver como criar um usuário com privilégios mínimos em PostgreSQL:

-- Criar um usuário com permissões limitadas
CREATE USER app_user WITH PASSWORD 'senha_segura';

-- Conceder apenas SELECT em uma tabela específica
GRANT SELECT ON TABLE usuarios TO app_user;

-- Conceder INSERT, UPDATE, DELETE em outra tabela
GRANT INSERT, UPDATE, DELETE ON TABLE pedidos TO app_user;

-- Não conceder privilégios de criação de tabelas ou banco
-- REVOKE ALL PRIVILEGES ON DATABASE meu_banco FROM app_user;

Em MySQL, a sintaxe é semelhante:

CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'senha_segura';
GRANT SELECT ON meu_banco.usuarios TO 'app_user'@'localhost';
GRANT SELECT, INSERT, UPDATE ON meu_banco.pedidos TO 'app_user'@'localhost';
FLUSH PRIVILEGES;

Além disso, é importante configurar a aplicação para usar essa conta limitada, e não a conta de administrador. Em ambientes de desenvolvimento, muitas vezes usamos o usuário root, mas em produção isso é um risco grave.

Escapes

Escapes (ou escaping) é uma técnica de sanitização que consiste em escapar caracteres especiais em strings antes de inseri-las em uma consulta SQL. Embora não seja tão segura quanto prepared statements, ainda é usada em alguns contextos, especialmente quando você precisa construir consultas dinâmicas de forma manual. O problema é que é fácil esquecer de escapar um caractere ou fazer isso de forma incorreta, abrindo brechas para injeção. Por isso, a recomendação moderna é evitar escapes e usar prepared statements ou ORMs.

No MySQL, a função mysqli_real_escape_string() escapa caracteres como aspas simples, aspas duplas e barras invertidas. Em PostgreSQL, a função pg_escape_string() faz algo similar. Em Python, você pode usar sqlite3 com parâmetros, mas se precisar escapar manualmente, pode usar replace() para substituir aspas simples por duas aspas simples (padrão SQL para escapar).

Exemplo em PHP com mysqli:

$conexao = new mysqli("localhost", "usuario", "senha", "banco");
$nome = $conexao->real_escape_string($_POST['nome']);
$sql = "SELECT * FROM usuarios WHERE nome = '$nome'";
$resultado = $conexao->query($sql);

Apesar de funcionar, essa abordagem é frágil. Se você esquecer de escapar uma variável, ou se o banco de dados tiver um comportamento diferente (como usar aspas duplas para strings), a injeção pode ocorrer. Portanto, use escapes apenas quando não houver alternativa, e prefira sempre prepared statements.

Boas práticas e observações finais

Além das técnicas vistas, algumas boas práticas ajudam a reforçar a segurança:

  • Nunca confie em dados vindos do cliente (formulários, headers, cookies) — trate tudo como não confiável.
  • Valide e sanitize os dados de entrada como primeira camada de defesa, mas lembre que isso não substitui prepared statements.
  • Mantenha o banco de dados atualizado e aplique as correções de segurança do fabricante.
  • Use um firewall de aplicação web (WAF) para detectar e bloquear tentativas de injeção.
  • Monitore logs de banco de dados para identificar consultas anômalas.

A segurança de SQL é um campo em constante evolução, mas os princípios fundamentais permanecem os mesmos: nunca concatene dados em SQL, use parâmetros, minimize privilégios e esteja sempre atento a novas ameaças.

Referências

Exercícios

  1. Explique por que prepared statements são mais seguros do que a concatenação de strings em consultas SQL. Dê um exemplo de código vulnerável e outro seguro em Python usando sqlite3.

✓ Resposta: Prepared statements separam o código SQL dos dados, enviando-os como parâmetros que são tratados literalmente pelo banco, impedindo que caracteres maliciosos sejam interpretados como parte da instrução. Exemplo vulnerável: cursor.execute(f"SELECT * FROM usuarios WHERE nome = '{nome}'"). Exemplo seguro: cursor.execute("SELECT * FROM usuarios WHERE nome = ?", (nome,)).
  • Como um ORM pode ajudar na prevenção de injeção de SQL? Cite um exemplo de uso seguro em uma linguagem de sua escolha.
  • ✓ Resposta: ORMs geralmente usam prepared statements internamente, mas é preciso evitar consultas cruas sem parametrização. Exemplo em Python com SQLAlchemy: conn.execute(text("SELECT * FROM usuarios WHERE nome = :nome"), {"nome": nome}).
  • Qual é o princípio do menor privilégio e como ele se aplica a contas de banco de dados? Dê um exemplo de criação de usuário com permissões limitadas em PostgreSQL.
  • ✓ Resposta: O princípio do menor privilégio diz que cada usuário deve ter apenas as permissões necessárias. Exemplo em PostgreSQL: CREATE USER app_user WITH PASSWORD 'senha'; GRANT SELECT ON TABLE usuarios TO app_user;
  • O que são escapes e por que eles são considerados menos seguros que prepared statements? Ilustre com um exemplo em PHP usando mysqli.
  • ✓ Resposta: Escapes sanitizam caracteres especiais, mas podem ser esquecidos ou mal implementados. Exemplo: $nome = $conexao->real_escape_string($_POST['nome']); $sql = "SELECT * FROM usuarios WHERE nome = '$nome'"; É melhor usar prepared statements.
  • Escreva uma consulta SQL segura usando prepared statements em uma linguagem de sua preferência para buscar um usuário por email e senha, e explique como ela protege contra injeção.
  • ✓ Resposta: Em Python: cursor.execute("SELECT * FROM usuarios WHERE email = ? AND senha = ?", (email, senha)). Os valores são parâmetros, então não podem alterar a estrutura do SQL.