Injeção SQL e Segurança

Injeção SQL e Segurança

🎯 Analogia da Vida Real

Imagine um ponto de retirada de encomendas:

📚 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.

SQL
-- 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

SQL
-- 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.

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
# 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()
JAVASCRIPT
// 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
// 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
// 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:

PYTHON
# ✅ 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])
JAVASCRIPT
// ✅ 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:

SQL
-- 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

SQL
-- 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;
💡 Dica: Nunca confie na entrada do usuário. Mesmo menus suspensos e campos ocultos podem ser alterados; o servidor deve validar e parametrizar todas as entradas.

Exemplo: Proteger um Endpoint de Login Contra Injeção (Dificuldade ⭐)

Antes (perigoso):

PYTHON
# 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"
▶ Experimente

Depois (seguro):

PYTHON
# 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:

JAVASCRIPT
// 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);
    });
});
▶ Experimente

Abordagem segura + otimizada:

JAVASCRIPT
// 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
# 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

JAVASCRIPT
// 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);
});
💡 Chave: Campos de ordenação não podem usar placeholders parametrizados diretamente (já que são nomes de colunas, não valores) e devem ser validados com uma lista de permissões.

❓ 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:

📝 Exercícios

  1. 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
    
  2. Escreva código seguro de consulta parametrizada para uma funcionalidade de pesquisa de usuários (suportando pesquisa fuzzy, paginação e ordenação).
  3. 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

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%