Makrosites

Suas ideias em realidade digital!

SQL Injection em PHP: como evitar com PDO e prepared statements

SQL Injection em PHP: como evitar com PDO e prepared statements >

SQL Injection é uma das falhas de segurança mais conhecidas em aplicações web. Ela acontece quando dados enviados pelo usuário são colocados diretamente dentro de uma consulta SQL sem tratamento adequado. Em PHP, esse problema costuma aparecer em sistemas de login, formulários de busca, cadastros, filtros, APIs e painéis administrativos.

A boa notícia é que evitar SQL Injection em PHP não é complicado quando usamos PDO com prepared statements. Com essa abordagem, os valores enviados pelo usuário são tratados como parâmetros, e não como parte direta do comando SQL.

Neste artigo, você vai entender o que é SQL Injection, ver exemplos de código vulnerável e aprender como corrigir usando PDO, parâmetros nomeados, validação de dados e boas práticas para proteger sua aplicação.

Resumo prático: nunca monte SQL concatenando diretamente valores vindos de $_GET, $_POST, JSON ou qualquer entrada externa. Use PDO com prepared statements.

O que é SQL Injection?

SQL Injection é uma técnica usada para manipular consultas SQL por meio de dados enviados pelo usuário. Quando a aplicação monta uma query misturando texto SQL com valores externos sem proteção, um atacante pode alterar o comportamento da consulta.

Em vez de enviar apenas um e-mail, nome ou ID, a pessoa pode enviar um trecho de SQL malicioso. Se o sistema concatenar esse valor diretamente na query, o banco pode interpretar o conteúdo como comando SQL.

Isso pode causar problemas como:

Exemplo de código PHP vulnerável

Veja um exemplo comum de código vulnerável em uma tela de login.

query($sql);
$usuario = $resultado->fetch();

Esse código é perigoso porque os valores de $email e $senha foram colocados diretamente dentro da query. Se alguém enviar um valor malicioso, a consulta pode ser manipulada.

Atenção: mesmo que exista validação no frontend com JavaScript, o backend ainda precisa proteger a consulta SQL. Qualquer pessoa pode chamar sua API ou formulário diretamente.

Por que concatenar SQL é perigoso?

O problema da concatenação é que o banco não consegue diferenciar com segurança o que é comando SQL e o que é valor enviado pelo usuário.

Exemplo ruim:

Se $id vier corretamente como 10, a consulta funciona. Mas se o valor vier malformado, a query pode se comportar de forma inesperada.

Por isso, dados externos nunca devem entrar diretamente dentro da string SQL.

Forma correta: usando PDO com prepared statements

A forma recomendada é preparar a consulta e enviar os valores separadamente.

prepare($sql);

$stmt->execute([
    ':id' => $id
]);

$usuario = $stmt->fetch();

Nesse exemplo, :id é um parâmetro nomeado. O valor real é enviado separadamente no execute.

Isso reduz o risco de SQL Injection porque o valor não é interpretado como parte do comando SQL.

Criando uma conexão PDO segura

Antes de usar prepared statements, vamos criar uma conexão PDO com boas configurações.

 PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );
} catch (PDOException $e) {
    error_log('Erro de conexão: ' . $e->getMessage());

    http_response_code(500);
    echo json_encode([
        'success' => false,
        'message' => 'Erro interno no servidor.'
    ]);
    exit;
}

A configuração PDO::ATTR_EMULATE_PREPARES => false ajuda o PDO a usar prepared statements reais quando suportado pelo driver.

Exemplo seguro de busca por ID

Para buscar um usuário por ID, podemos validar o parâmetro e usar bindValue.

 false,
        'message' => 'ID inválido.'
    ]);
    exit;
}

$sql = "SELECT id, nome, email FROM usuarios WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();

$usuario = $stmt->fetch();

if (!$usuario) {
    http_response_code(404);
    echo json_encode([
        'success' => false,
        'message' => 'Usuário não encontrado.'
    ]);
    exit;
}

echo json_encode([
    'success' => true,
    'data' => $usuario
]);

Aqui temos duas camadas de proteção: validação do ID e prepared statement.

Exemplo seguro de login com PHP e PDO

Em sistemas de login, além de evitar SQL Injection, também é importante nunca comparar senha em texto puro. O correto é salvar a senha com password_hash e validar com password_verify.

 false,
        'message' => 'E-mail e senha são obrigatórios.'
    ]);
    exit;
}

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    http_response_code(422);
    echo json_encode([
        'success' => false,
        'message' => 'Informe um e-mail válido.'
    ]);
    exit;
}

$sql = "SELECT id, nome, email, senha FROM usuarios WHERE email = :email LIMIT 1";
$stmt = $pdo->prepare($sql);
$stmt->execute([
    ':email' => $email
]);

$usuario = $stmt->fetch();

if (!$usuario || !password_verify($senha, $usuario['senha'])) {
    http_response_code(401);
    echo json_encode([
        'success' => false,
        'message' => 'E-mail ou senha inválidos.'
    ]);
    exit;
}

echo json_encode([
    'success' => true,
    'message' => 'Login realizado com sucesso.',
    'data' => [
        'id' => $usuario['id'],
        'nome' => $usuario['nome'],
        'email' => $usuario['email']
    ]
]);

Repare que a senha não entra na query. Primeiro buscamos o usuário pelo e-mail com segurança. Depois validamos a senha usando password_verify.

Exemplo seguro de cadastro com INSERT

Para cadastrar dados, também devemos usar prepared statements.

 false,
        'message' => 'Preencha todos os campos obrigatórios.'
    ]);
    exit;
}

$senhaHash = password_hash($senha, PASSWORD_DEFAULT);

$sql = "INSERT INTO usuarios (nome, email, senha) VALUES (:nome, :email, :senha)";
$stmt = $pdo->prepare($sql);

$stmt->execute([
    ':nome' => $nome,
    ':email' => $email,
    ':senha' => $senhaHash
]);

echo json_encode([
    'success' => true,
    'message' => 'Usuário cadastrado com sucesso.',
    'data' => [
        'id' => $pdo->lastInsertId()
    ]
]);

Mesmo sendo um INSERT, os dados externos continuam sendo enviados como parâmetros.

Exemplo seguro de UPDATE

Atualizações também devem ser protegidas.

 false,
        'message' => 'ID inválido.'
    ]);
    exit;
}

$sql = "UPDATE usuarios SET nome = :nome, email = :email WHERE id = :id";
$stmt = $pdo->prepare($sql);

$stmt->bindValue(':nome', $nome);
$stmt->bindValue(':email', $email);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);

$stmt->execute();

echo json_encode([
    'success' => true,
    'message' => 'Usuário atualizado com sucesso.'
]);

O id foi tratado como inteiro e os demais campos foram passados como parâmetros.

Exemplo seguro de DELETE

Para excluir registros, nunca use o ID diretamente na query.

 false,
        'message' => 'ID inválido.'
    ]);
    exit;
}

$sql = "DELETE FROM usuarios WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
$stmt->execute();

echo json_encode([
    'success' => true,
    'message' => 'Usuário excluído com sucesso.'
]);

Em sistemas reais, muitas vezes é melhor usar exclusão lógica, como um campo deleted_at ou ativo, em vez de apagar definitivamente.

Busca com LIKE usando prepared statement

Um erro comum é achar que LIKE não pode ser usado com prepared statements. Pode sim, desde que o parâmetro seja montado corretamente.

prepare($sql);

$stmt->execute([
    ':busca' => '%' . $busca . '%'
]);

$resultados = $stmt->fetchAll();

echo json_encode([
    'success' => true,
    'data' => $resultados
]);

O valor com % é passado como parâmetro, não concatenado dentro da query.

Filtro com múltiplos parâmetros

Em APIs com filtros, você pode montar condições dinamicamente, mas os valores devem continuar parametrizados.

prepare($sql);
$stmt->execute($params);

$usuarios = $stmt->fetchAll();

Esse modelo é útil para APIs com busca, filtros por status, categoria, data, usuário ou tipo.

Cuidado com ORDER BY dinâmico

Parâmetros de ordenação exigem cuidado especial. Em geral, não é possível parametrizar nomes de colunas da mesma forma que valores. Por isso, use uma lista branca.

Como order_by e order_dir entram na estrutura da query, eles precisam ser limitados a valores conhecidos.

Usar addslashes evita SQL Injection?

Não é o ideal. Funções como addslashes ou tentativas manuais de escapar strings não substituem prepared statements.

O problema é que escapar manualmente pode falhar dependendo do banco, charset, driver e contexto da consulta. Com PDO preparado corretamente, você separa o comando SQL dos valores.

Regra prática: não tente criar sua própria proteção contra SQL Injection usando apenas substituições de texto. Use prepared statements.

Validação de dados também é importante

Prepared statement protege a consulta, mas não substitui validação de dados. Mesmo usando PDO, você ainda deve validar se os valores fazem sentido.

Exemplos:

  • ID deve ser inteiro positivo;
  • E-mail deve ter formato válido;
  • Senha deve ter tamanho mínimo;
  • Status deve estar dentro de uma lista permitida;
  • Datas devem ter formato correto;
  • Campos obrigatórios não podem estar vazios.

Segurança boa combina prepared statements, validação, controle de acesso e tratamento de erros.

Não exiba erro SQL para o usuário

Em produção, mensagens de erro do banco não devem aparecer na resposta da API ou na tela do usuário.

Evite retornar algo como:

SQLSTATE[42S22]: Column not found: 1054 Unknown column...

Esse tipo de mensagem pode revelar detalhes internos da aplicação. O melhor é registrar o erro em log e retornar uma mensagem genérica.

prepare($sql);
    $stmt->execute($params);
} catch (PDOException $e) {
    error_log('Erro SQL: ' . $e->getMessage());

    http_response_code(500);
    echo json_encode([
        'success' => false,
        'message' => 'Erro interno no servidor.'
    ]);
    exit;
}

Checklist para evitar SQL Injection em PHP

  • Não concatene valores externos diretamente no SQL;
  • Use PDO com prepared statements;
  • Use parâmetros nomeados ou posicionais;
  • Valide IDs como inteiros;
  • Valide e-mails, datas, status e campos obrigatórios;
  • Use lista branca para colunas em ORDER BY;
  • Não use addslashes como solução principal;
  • Não exiba erros SQL para o usuário final;
  • Registre falhas em log;
  • Use password_hash e password_verify em logins;
  • Restrinja permissões do usuário do banco;
  • Mantenha PHP, banco e dependências atualizados.

Permissões do usuário do banco também importam

Além de proteger o código, é importante configurar o usuário do banco com as permissões necessárias e nada além disso.

Por exemplo, se uma aplicação só precisa ler dados em determinada área, não faz sentido usar um usuário com permissão para excluir tabelas.

Reduzir privilégios ajuda a limitar danos caso algum problema de segurança aconteça.

Erros comuns ao tentar evitar SQL Injection

1. Achar que validação no frontend é suficiente

O frontend pode ser ignorado facilmente. A proteção precisa estar no backend.

2. Usar concatenação porque “é só um ID”

Mesmo IDs devem ser validados e enviados como parâmetros.

3. Usar addslashes como proteção principal

Escapar manualmente não é tão seguro quanto usar prepared statements.

4. Parametrizar valores, mas não validar ORDER BY

Colunas e direções de ordenação devem passar por lista branca.

5. Mostrar erro real do banco em produção

Mensagens internas devem ir para logs, não para a resposta pública.

Exemplo completo de endpoint protegido

Abaixo temos um endpoint completo de listagem de usuários com busca, status, ordenação segura e prepared statements.

 PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );

    $busca = isset($_GET['busca']) ? trim($_GET['busca']) : '';
    $status = isset($_GET['status']) ? trim($_GET['status']) : '';
    $orderBy = $_GET['order_by'] ?? 'id';
    $orderDir = strtolower($_GET['order_dir'] ?? 'desc');

    $colunasPermitidas = ['id', 'nome', 'email', 'created_at'];
    $direcoesPermitidas = ['asc', 'desc'];

    if (!in_array($orderBy, $colunasPermitidas, true)) {
        $orderBy = 'id';
    }

    if (!in_array($orderDir, $direcoesPermitidas, true)) {
        $orderDir = 'desc';
    }

    $where = [];
    $params = [];

    if ($busca !== '') {
        $where[] = "(nome LIKE :busca OR email LIKE :busca)";
        $params[':busca'] = '%' . $busca . '%';
    }

    if ($status !== '') {
        $statusPermitidos = ['ativo', 'inativo'];

        if (!in_array($status, $statusPermitidos, true)) {
            jsonResponse(422, [
                'success' => false,
                'message' => 'Status inválido.'
            ]);
        }

        $where[] = "status = :status";
        $params[':status'] = $status;
    }

    $whereSql = '';

    if (!empty($where)) {
        $whereSql = 'WHERE ' . implode(' AND ', $where);
    }

    $sql = "
        SELECT id, nome, email, status, created_at
        FROM usuarios
        {$whereSql}
        ORDER BY {$orderBy} {$orderDir}
        LIMIT 50
    ";

    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);

    $usuarios = $stmt->fetchAll();

    jsonResponse(200, [
        'success' => true,
        'data' => $usuarios
    ]);

} catch (PDOException $e) {
    error_log('Erro PDO: ' . $e->getMessage());

    jsonResponse(500, [
        'success' => false,
        'message' => 'Erro interno ao consultar os dados.'
    ]);

} catch (Throwable $e) {
    error_log('Erro inesperado: ' . $e->getMessage());

    jsonResponse(500, [
        'success' => false,
        'message' => 'Erro interno no servidor.'
    ]);
}

Conclusão

SQL Injection é uma falha grave, mas pode ser evitada com práticas corretas no PHP. A principal regra é nunca concatenar valores externos diretamente na consulta SQL.

Usando PDO com prepared statements, validação de dados, lista branca para ordenação e tratamento adequado de erros, sua aplicação fica muito mais segura contra manipulações de consulta.

Essa proteção deve ser aplicada em login, cadastro, filtros, buscas, APIs, painéis administrativos e qualquer ponto do sistema que interaja com o banco de dados.

Perguntas frequentes sobre SQL Injection em PHP

O que é SQL Injection em PHP?

SQL Injection é uma falha que acontece quando dados enviados pelo usuário são inseridos diretamente em uma consulta SQL, permitindo manipular o comportamento da query.

Como evitar SQL Injection em PHP?

A forma mais recomendada é usar PDO com prepared statements, passando os valores como parâmetros em vez de concatenar diretamente no SQL.

PDO evita SQL Injection?

Sim, quando usado corretamente com prepared statements e parâmetros. O PDO ajuda a separar o comando SQL dos valores enviados pelo usuário.

addslashes protege contra SQL Injection?

Não deve ser usado como proteção principal. O ideal é usar prepared statements em vez de tentar escapar manualmente os valores.

Preciso validar os dados mesmo usando prepared statements?

Sim. Prepared statements protegem a consulta, mas a validação garante que os dados façam sentido para a regra da aplicação.

LIKE pode ser usado com prepared statement?

Sim. Basta montar o valor com % e passá-lo como parâmetro, por exemplo '%' . $busca . '%'.