Injeção SQL e Segurança
Injeção SQL e Segurança
🎯 Analogia da Vida Real
Imagine um ponto de retirada de encomendas:
- Fluxo normal: Você diz "Retirar a encomenda de João Silva", e o funcionário pesquisa pelo nome → consulta normal
- Ataque de injeção: Você diz "Retirar a encomenda de João Silva, e também me dê a de Maria Santos", e o funcionário não distingue cuidadosamente, entregando ambas as encomendas → injeção SQL
- Medidas de defesa: O funcionário exige que você preencha um formulário completo de retirada, com cada campo preenchido separadamente, não aceitando solicitações concatenadas verbalmente → consulta parametrizada
📚 Conceitos Principais
1. Princípio da Injeção SQL
A essência da injeção SQL é: a entrada do usuário é executada como código SQL.
Quando um programa constrói SQL usando concatenação de strings, atacantes podem inserir fragmentos SQL maliciosos.
-- Código backend (pseudocódigo)
sql = "SELECT * FROM users WHERE username = '" + input_user + "' AND password = '" + input_pass + "'"
-- Entrada normal
-- input_user = "admin"
-- input_pass = "123456"
-- SQL gerado:
SELECT * FROM users WHERE username = 'admin' AND password = '123456'
-- Entrada maliciosa
-- input_user = "admin' --"
-- input_pass = "qualquer coisa"
-- SQL gerado:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'qualquer coisa'
-- '--' é um comentário SQL, o restante da condição é comentado, ignorando a verificação de senha!
2. Técnicas Comuns de Ataque por Injeção
-- 1. Ignorar verificação de login
-- Entrada: admin' OR '1'='1' --
SELECT * FROM users WHERE username = 'admin' OR '1'='1' --' AND password = ''
-- 2. Consulta UNION para roubar dados
-- Entrada: ' UNION SELECT username, password FROM users --
SELECT * FROM products WHERE name = '' UNION SELECT username, password FROM users --'
-- 3. Consultas empilhadas para executar múltiplas instruções
-- Entrada: '; DROP TABLE users; --
SELECT * FROM products WHERE name = ''; DROP TABLE users; --'
-- 4. Injeção booleana cega (inferir dados através de diferenças na resposta)
-- Entrada: ' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --
SELECT * FROM products WHERE name = '' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' --'
3. Consulta Parametrizada (Prepared Statement)
Ideia central: A estrutura SQL e os dados são separados; o mecanismo do banco de dados não executará valores de parâmetros como código SQL.
-- ❌ Concatenação de strings (perigoso)
SET @username = "admin' --";
SET @sql = CONCAT('SELECT * FROM users WHERE username = ''', @username, '''');
PREPARE stmt FROM @sql;
EXECUTE stmt;
-- ✅ Consulta parametrizada (segura)
SET @username = "admin' --";
PREPARE stmt FROM 'SELECT * FROM users WHERE username = ?';
EXECUTE stmt USING @username;
-- O banco de dados trata todo @username como um único valor de string, não executando nenhum fragmento SQL dentro dele
Consultas parametrizadas em várias linguagens:
# Python + MySQL Connector
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
# Python + SQLAlchemy ORM
user = session.query(User).filter(User.username == username, User.password == password).first()
// Node.js + mysql2
const [rows] = await pool.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);
// Node.js + Prisma ORM
const user = await prisma.user.findFirst({ where: { username, password } });
// Java JDBC
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
// PHP PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute(['username' => $username, 'password' => $password]);
4. Prevenção de Injeção com ORM
Frameworks ORM (Object-Relational Mapping) normalmente usam consultas parametrizadas automaticamente, mas cautela ainda é necessária ao concatenar SQL bruto:
# ✅ Parametrização automática do ORM (seguro)
User.objects.filter(username=username, password=password)
# ❌ Concatenando SQL bruto no ORM (perigoso)
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")
# ✅ Maneira segura de usar SQL bruto no ORM
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])
// ✅ Parametrização automática do Sequelize ORM
await User.findOne({ where: { username, password } });
// ❌ Consulta concatenada do Sequelize (perigoso)
await sequelize.query(`SELECT * FROM users WHERE username = '${username}'`);
// ✅ Abordagem segura do Sequelize
await sequelize.query("SELECT * FROM users WHERE username = ?", { replacements: [username] });
5. Princípio do Menor Privilégio
Usuários do banco de dados devem receber apenas os privilégios mínimos necessários para completar suas tarefas:
-- Criar usuário somente leitura
CREATE USER 'app_readonly'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON mydb.* TO 'app_readonly'@'%';
-- Criar usuário de aplicação (permitir apenas CRUD, sem DROP/ALTER)
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'%';
-- Prevenir propagação de privilégios
REVOKE GRANT OPTION ON mydb.* FROM 'app_user'@'%';
-- Usuário administrador (apenas gerenciamento interno)
CREATE USER 'db_admin'@'localhost' IDENTIFIED BY 'very_strong_password';
GRANT ALL PRIVILEGES ON mydb.* TO 'db_admin'@'localhost';
6. Lista de Verificação de Defesa
| Medida de Defesa | Descrição |
|---|---|
| ✅ Consultas parametrizadas | Usar placeholders de parâmetros para todas as consultas SQL |
| ✅ Frameworks ORM | Preferir ORM para reduzir SQL escrito manualmente |
| ✅ Validação de entrada | Validar comprimento, tipo, formato (lista de permissões em vez de lista de bloqueio) |
| ✅ Menor privilégio | Conceder apenas permissões necessárias aos usuários do banco de dados |
| ✅ Mensagens de erro | Não expor detalhes de erros do banco de dados aos usuários |
| ✅ Proteção WAF | Implementar um Web Application Firewall |
| ✅ Auditorias regulares | Verificar código em busca de concatenação SQL |
| ❌ Escape de strings | Não é confiável o suficiente, não deve ser a única defesa |
💡 Sintaxe Básica
-- Prepared Statement do MySQL
PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?';
SET @id = 1;
EXECUTE stmt USING @id;
DEALLOCATE PREPARE stmt;
-- Abordagem segura para consultas LIKE no MySQL
PREPARE stmt FROM 'SELECT * FROM users WHERE name LIKE ?';
SET @pattern = CONCAT('%', 'keyword', '%');
EXECUTE stmt USING @pattern;
-- Abordagem segura para cláusulas IN (requer construção dinâmica)
-- Método: Usar FIND_IN_SET ou uma tabela temporária
PREPARE stmt FROM 'SELECT * FROM users WHERE FIND_IN_SET(id, ?)';
SET @ids = '1,2,3';
EXECUTE stmt USING @ids;
Exemplo: Proteger um Endpoint de Login Contra Injeção (Dificuldade ⭐)
Antes (perigoso):
# Python Flask - abordagem perigosa
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# ❌ Concatenação de strings, vulnerável a injeção SQL
sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
user = db.execute(sql).fetchone()
if user:
return "Login bem-sucedido"
return "Nome de usuário ou senha inválidos"
Depois (seguro):
# Python Flask - abordagem segura
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# Validação de entrada
if not username or len(username) > 50:
return "Formato de nome de usuário inválido", 400
if not password or len(password) > 100:
return "Formato de senha inválido", 400
# ✅ Consulta parametrizada
user = db.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
(username, password)
).fetchone()
if user:
return "Login bem-sucedido"
return "Nome de usuário ou senha inválidos"
Exemplo: Proteger Pesquisa com Prevenção de Injeção e Otimização de Performance (Dificuldade ⭐⭐)
Abordagem perigosa:
// Node.js Express - abordagem perigosa
app.get('/search', (req, res) => {
const keyword = req.query.q;
// ❌ Concatenação SQL, vulnerável a injeção
const sql = `SELECT * FROM articles WHERE title LIKE '%${keyword}%' OR content LIKE '%${keyword}%'`;
db.query(sql, (err, results) => {
res.json(results);
});
});
Abordagem segura + otimizada:
// Node.js Express - abordagem segura + otimizada
app.get('/search', async (req, res) => {
try {
const keyword = req.query.q?.trim();
// Validação de entrada
if (!keyword || keyword.length > 100) {
return res.status(400).json({ error: 'A palavra-chave de pesquisa deve ter 1-100 caracteres' });
}
// ✅ Consulta parametrizada + otimização de índice de texto completo
const sql = `
SELECT id, title, LEFT(content, 200) AS snippet, created_at
FROM articles
WHERE status = 1
AND MATCH(title, content) AGAINST(? IN BOOLEAN MODE)
ORDER BY MATCH(title, content) AGAINST(? IN BOOLEAN MODE) DESC
LIMIT 20
`;
const [rows] = await pool.execute(sql, [keyword, keyword]);
res.json(rows);
} catch (err) {
// Não expor detalhes de erros do banco de dados
console.error('Erro de pesquisa:', err);
res.status(500).json({ error: 'Serviço de pesquisa temporariamente indisponível' });
}
});
🏢 Cenário 1: Segurança do Endpoint de Registro de Usuário
# Python - Registro seguro de usuário
import re
import hashlib
@app.route('/register', methods=['POST'])
def register():
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
# 1. Validação de entrada (lista de permissões)
if not re.match(r'^[a-zA-Z0-9_]{3,50}$', username):
return "O nome de usuário pode conter apenas letras, números e underscores, 3-50 caracteres", 400
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email) or len(email) > 100:
return "Formato de e-mail inválido", 400
if len(password) < 8 or len(password) > 100:
return "A senha deve ter 8-100 caracteres", 400
# 2. Consulta parametrizada para verificar existência
existing = db.execute(
"SELECT id FROM users WHERE username = ? OR email = ?",
(username, email)
).fetchone()
if existing:
return "Nome de usuário ou e-mail já registrado", 409
# 3. Armazenamento de hash de senha
password_hash = hashlib.sha256(password.encode()).hexdigest()
# 4. Inserção parametrizada
db.execute(
"INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
(username, email, password_hash)
)
db.commit()
return "Registro bem-sucedido", 201
🏢 Cenário 2: Construção Segura de Condições de Filtro Dinâmico
// Node.js - Construção segura de consulta dinâmica
function buildProductQuery(filters) {
let sql = 'SELECT * FROM products WHERE 1=1';
const params = [];
if (filters.categoryId) {
sql += ' AND category_id = ?';
params.push(filters.categoryId);
}
if (filters.minPrice !== undefined) {
sql += ' AND price >= ?';
params.push(filters.minPrice);
}
if (filters.maxPrice !== undefined) {
sql += ' AND price <= ?';
params.push(filters.maxPrice);
}
if (filters.keyword) {
sql += ' AND name LIKE ?';
params.push(`%${filters.keyword}%`);
}
if (filters.status !== undefined) {
sql += ' AND status = ?';
params.push(filters.status);
}
// Lista de permissões para campos de ordenação
const allowedSort = ['price', 'created_at', 'sales_count'];
const sortBy = allowedSort.includes(filters.sortBy) ? filters.sortBy : 'created_at';
const sortOrder = filters.sortOrder === 'ASC' ? 'ASC' : 'DESC';
sql += ` ORDER BY ${sortBy} ${sortOrder}`;
// Validação de parâmetros de paginação
const page = Math.max(1, parseInt(filters.page) || 1);
const pageSize = Math.min(100, Math.max(1, parseInt(filters.pageSize) || 20));
sql += ' LIMIT ? OFFSET ?';
params.push(pageSize, (page - 1) * pageSize);
return { sql, params };
}
// Uso
app.get('/products', async (req, res) => {
const { sql, params } = buildProductQuery(req.query);
const [rows] = await pool.execute(sql, params);
res.json(rows);
});
❓ Perguntas Frequentes
P: Consultas parametrizadas afetam a performance? R: Não. Consultas parametrizadas também podem aproveitar o cache de pré-compilação do banco de dados, tornando execuções repetidas do mesmo modelo de consulta mais rápidas.
P: Frameworks ORM são sempre seguros? R: Não necessariamente. A parametrização automática do ORM é segura, mas se você usar
raw()ou concatenação de strings para construir consultas, o risco de injeção ainda existe. Frameworks são apenas ferramentas; a conscientização de segurança é o que importa.
P: Procedures armazenadas podem prevenir injeção? R: Procedures armazenadas em si não previnem injeção. Se uma procedure armazenada usa concatenação SQL dinâmica internamente, ela é igualmente vulnerável. A chave é se a parametrização é usada.
P: Apenas validação no frontend é suficiente? R: Absolutamente não. A validação no frontend pode ser ignorada (chamando APIs diretamente, modificando requisições). Toda validação de segurança deve ser realizada no lado do servidor.
📖 Resumo
Esta lição cobriu profundamente ataques e defesas de injeção SQL:
- Princípio de injeção: A entrada do usuário é executada como código SQL, enraizada na concatenação de strings
- Técnicas de ataque: Bypass de login, consultas UNION, consultas empilhadas, injeção cega, etc.
- Defesa central: Consultas parametrizadas (Prepared Statements), separando a estrutura SQL dos dados
- Frameworks ORM: Parametrização automática, mas ainda vigilante ao concatenar SQL bruto
- Menor privilégio: Conceder apenas permissões necessárias aos usuários do banco de dados; nunca usar root para conexões de aplicação
- Defesa em profundidade: Parametrização + validação de entrada + controle de acesso + ocultação de erros + WAF
📝 Exercícios
- Encontre a vulnerabilidade de injeção SQL no código abaixo e corrija-o:SQL
query = "SELECT * FROM products WHERE category = '" + category + "' ORDER BY " + sortField - Escreva código seguro de consulta parametrizada para uma funcionalidade de pesquisa de usuários (suportando pesquisa fuzzy, paginação e ordenação).
- Crie três usuários de banco de dados para seu projeto: um usuário somente leitura, um usuário de aplicação e um usuário administrador, concedendo permissões apropriadas a cada um.
Próxima Lição →27-optimization.md



