Prepared statements (PDO) são a defesa correta contra SQL injection em PHP.

SQL injection em PHP: use PDO e prepared statements

Segurança web não negocia. Como arquiteto que vi invasões silenciosas e bases inteiras exportadas via login mal implementado, revisito SQL injection em PHP porque o ataque continua trivial onde desenvolvedores concatenam input do usuário em query. Meu post original recomendava addslashes; hoje, em 2026, isso é insuficiente e enganoso. Este artigo reescreve a defesa correta: PDO com prepared statements, validação de entrada e princípio de least privilege no banco — espelhando o que já defendo para Java em PreparedStatement Java.

Por que addslashes falha

addslashes escapa aspas para contexto de string SQL literal — frágil quando encoding muda, quando query usa numeric context sem aspas, quando segundo order injection aparece em ORDER BY, ou quando ORM/raw mix esquece escape. Atacante envia ' OR '1'='1 ou variantes unicode e autentica como primeiro usuário retornado. Não é teoria: scanners automatizados testam isso em minutos.

Defesa correta: prepared statements com PDO

PDO (PHP Data Objects) com placeholders vinculados separa estrutura SQL de dados. O motor trata parâmetros como valor, não como código executável.

$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

$sql = 'SELECT id, nome FROM tbusuario WHERE login = :login AND ativo = 1';
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':login', $_POST['login'] ?? '', PDO::PARAM_STR);
$stmt->execute();
$user = $stmt->fetch();

Nunca interpole variável PHP dentro da string SQL. Se precisa nome de coluna dinâmico — raro — use whitelist, não concatenação livre.

Login seguro completo

  1. Prepared statement para busca por login (nunca senha em SQL comparando texto plano).
  2. Verificação de senha com password_verify contra hash bcrypt/argon2.
  3. Rate limiting e lockout progressivo por IP/login.
  4. Sessão regenerada após auth (session_regenerate_id).
  5. Cookie HttpOnly, Secure, SameSite=Lax ou Strict.
if ($user && password_verify($_POST['senha'] ?? '', $user['senha_hash'])) {
    session_regenerate_id(true);
    $_SESSION['user_id'] = (int) $user['id'];
}

Consultas de pesquisa

LIKE com wildcard exige cuidado: bind do termo, escape de % e _ no lado aplicacao se usuário pode digitá-los. Paginação: bind limit/offset como inteiros (PDO::PARAM_INT), não concatene $_GET['page'] direto.

Erros que vazam schema

PDO::ERRMODE_EXCEPTION em dev; em produção, log interno + mensagem genérica ao usuário. Display_errors off no php.ini de produção — reforço em segurança web 2026.

Conta de banco dedicada

App web não precisa GRANT DROP DATABASE. Usuário SQL só com SELECT/INSERT/UPDATE/DELETE nas tabelas necessárias. SQL injection parcialmente contido se permissões forem mínimas.

ORMs (Eloquent, Doctrine)

ORM não imuniza: whereRaw("col = '$x'") reabre buraco. Use query builder parametrizado. Audite raw queries em code review.

Comparação com Java

Mecanismo idêntico ao PreparedStatement: placeholders posicionais ou nomeados, tipagem explícita, pool de conexões. Equipes híbridas PHP/Java devem padronizar checklist de code review SQL.

Testes

Teste integração envia payloads OWASP Top 10; asserta 401/400, não 500 com stack. Ferramentas: PHPUnit + banco efêmero Docker. CI bloqueia merge se teste segurança falhar — veja CI/CD times pequenos.

Migração legacy mysql_*

Código antigo com mysql_query e concatenação: migre módulo a módulo para PDO. Wrapper temporário centraliza escape NÃO substitui prepare — apenas ganha tempo para refactor.

Segunda ordem e ORDER BY

Whitelist colunas ordenáveis: $allowed = ['nome','data']; $col = in_array($_GET['sort'], $allowed) ? $_GET['sort'] : 'nome'; nunca bind param em identificador SQL. Direction ASC/DESC idem whitelist.

Stored procedures

CALL proc_x(?) com bind seguro. Corpo procedure ainda pode ser vulnerável se concatena internamente — revise DBA side também.

Logging e auditoria

Registre tentativa login falha sem armazenar senha plaintext. Alerta brute force Correlation id request. LGPD retention 90 dias logs acesso.

Content Security Policy complementar

PDO bloqueia SQLi server-side; CSP default-src self reduz XSS que exfiltra cookie session mesmo SQL seguro. Headers irmãos artigo segurança 2026.

PHP 8.3 strict types

declare(strict_types=1) funções domínio; tipagem reduz coercion estranha passada query. Combined com enum status pedido whitelist.

Auditoria OWASP ZAP CI

Baseline scan staging pull request — falha merge se SQLi detectado formulário login esquecido. Complementa PHPUnit prepared assertions.

Framework moderno Laravel/Symfony

Eloquent e Doctrine QueryBuilder parametrizam por default — perigo está raw where. Code review checklist item: grep "whereRaw|DB::select|" concatenation sql. PHPStan psalm nivel strict catch string sql parcial.

Upload arquivo: validate mime extension whitelist store outside webroot random name — SQLi irmão XSS file inclusion. Session fixation regenerate id login success always.

Defense in depth camadas

WAF Cloudflare OWASP ruleset bloqueia payload óbvio antes PHP. Prepared statement última linha se WAF bypass. Monitor slow query log padrão union select. Camada única falha; três camadas independentes probabilidade exploração real cai ordens magnitude.

Checklist deploy PHP produção

display_errors Off log_errors On error_log path rotacionado. expose_php Off disable_functions perigosa avalie risco. open_basedir restrito documentroot uploads tmp separado. OPcache enable validate_timestamps Off produção deploy reload php-fpm graceful. Headers segurança nginx snippet include once todas vhosts cliente TEK.

Revisão trimestral composer audit npm equivalent php outdated packages. PHP 8.3 EOL track calendar upgrade staging antes produção security patches.

Conclusão

Bloquear SQL injection em PHP exige prepared statements, hashing moderno de senha e hygiene operacional — não addslashes. Atualize tutoriais antigos, treine juniors com exemplo PDO desde o dia um e linke mentalmente com Java PreparedStatement. Na TEK Softwares, login legacy passou audit zero addslashes em uma tarde de refactor focado — ROI imediato em sono tranquilo.

WhatsApp