PHP

Como Usar Prepared Statements e Prevenção de SQL Injection em Produção

8 min de leitura

Como Usar Prepared Statements e Prevenção de SQL Injection em Produção

O Problema: SQL Injection SQL Injection é uma das vulnerabilidades mais críticas em aplicações web. Ocorre quando um atacante consegue inserir comandos SQL maliciosos através de inputs que não são validados adequadamente. Imagine um formulário de login simples: se o desenvolvedor concatenar strings diretamente na query, um atacante pode alterar completamente a lógica SQL. Considere este exemplo vulnerável em PHP com MySQLi procedural: Se o atacante inserir no campo usuário, a query fica: . Isso retorna todos os usuários, pulando a autenticação completamente. A consecução é devastadora: roubo de dados, deleção de registros, escalação de privilégios. Prepared Statements: A Solução Padrão Prepared Statements (ou consultas preparadas) separaram a estrutura SQL dos dados. O servidor de banco de dados primeiro recebe a estrutura da query com placeholders, depois os dados são enviados separadamente. Isso garante que dados nunca sejam interpretados como código SQL. Como Funcionam A query é enviada em duas etapas. Primeiro, o template é compilado no servidor. Segundo, os

<h2>O Problema: SQL Injection</h2>

<p>SQL Injection é uma das vulnerabilidades mais críticas em aplicações web. Ocorre quando um atacante consegue inserir comandos SQL maliciosos através de inputs que não são validados adequadamente. Imagine um formulário de login simples: se o desenvolvedor concatenar strings diretamente na query, um atacante pode alterar completamente a lógica SQL.</p>

<p>Considere este exemplo vulnerável em PHP com MySQLi procedural:</p>

<pre><code class="language-php">&lt;?php

$usuario = $_POST[&#039;usuario&#039;];

$senha = $_POST[&#039;senha&#039;];

// NUNCA FAÇA ASSIM!

$query = &quot;SELECT * FROM usuarios WHERE usuario = &#039;&quot; . $usuario . &quot;&#039; AND senha = &#039;&quot; . $senha . &quot;&#039;&quot;;

$resultado = mysqli_query($conexao, $query);

?&gt;</code></pre>

<p>Se o atacante inserir <code>&#039; OR &#039;1&#039;=&#039;1</code> no campo usuário, a query fica: <code>SELECT * FROM usuarios WHERE usuario = &#039;&#039; OR &#039;1&#039;=&#039;1&#039; AND senha = &#039;...&#039;</code>. Isso retorna todos os usuários, pulando a autenticação completamente. A consecução é devastadora: roubo de dados, deleção de registros, escalação de privilégios.</p>

<h2>Prepared Statements: A Solução Padrão</h2>

<p>Prepared Statements (ou consultas preparadas) separaram a estrutura SQL dos dados. O servidor de banco de dados primeiro recebe a estrutura da query com placeholders, depois os dados são enviados separadamente. Isso garante que dados nunca sejam interpretados como código SQL.</p>

<h3>Como Funcionam</h3>

<p>A query é enviada em duas etapas. Primeiro, o template é compilado no servidor. Segundo, os parâmetros são passados de forma segura. O banco de dados sabe exatamente qual é a estrutura e qual é o dado, impossibilitando injeção.</p>

<h3>Implementação em PHP com MySQLi</h3>

<pre><code class="language-php">&lt;?php

$conexao = mysqli_connect(&quot;localhost&quot;, &quot;user&quot;, &quot;password&quot;, &quot;database&quot;);

$usuario = $_POST[&#039;usuario&#039;];

$senha = $_POST[&#039;senha&#039;];

// Prepared Statement com placeholders (?)

$stmt = mysqli_prepare($conexao, &quot;SELECT * FROM usuarios WHERE usuario = ? AND senha = ?&quot;);

mysqli_stmt_bind_param($stmt, &quot;ss&quot;, $usuario, $senha);

mysqli_stmt_execute($stmt);

$resultado = mysqli_stmt_get_result($stmt);

while($row = mysqli_fetch_assoc($resultado)) {

echo &quot;Bem-vindo: &quot; . htmlspecialchars($row[&#039;usuario&#039;]);

}

mysqli_stmt_close($stmt);

mysqli_close($conexao);

?&gt;</code></pre>

<p>O <code>&quot;ss&quot;</code> indica que esperamos duas strings. As variáveis são passadas por referência e vinculadas aos placeholders antes da execução. Mesmo que o usuário digite <code>&#039; OR &#039;1&#039;=&#039;1</code>, será tratado como string literal.</p>

<h3>Implementação em Python com SQLite</h3>

<pre><code class="language-python">import sqlite3

conexao = sqlite3.connect(&#039;banco.db&#039;)

cursor = conexao.cursor()

usuario = input(&quot;Usuário: &quot;)

senha = input(&quot;Senha: &quot;)

Prepared Statement com ? placeholders

query = &quot;SELECT * FROM usuarios WHERE usuario = ? AND senha = ?&quot;

cursor.execute(query, (usuario, senha))

for linha in cursor.fetchall():

print(f&quot;Bem-vindo: {linha[0]}&quot;)

conexao.close()</code></pre>

<p>Python com o módulo <code>sqlite3</code> também usa prepared statements nativamente. Os dados são passados como tupla separada da query. O banco de dados nunca interpreta <code>(usuario, senha)</code> como código.</p>

<h3>Implementação com ORM (Django/SQLAlchemy)</h3>

<pre><code class="language-python">from django.db import models

from django.contrib.auth.models import User

Em Django, queries via ORM usam prepared statements automaticamente

usuario = User.objects.filter(username=usuario_input, password=senha_input).first()

Equivalente manual seria:

User.objects.raw(&quot;SELECT * FROM auth_user WHERE username = %s AND password = %s&quot;, [usuario_input, senha_input])</code></pre>

<p>ORMs (Object-Relational Mapping) abstraem prepared statements e são mais seguras por padrão. Evite usar <code>.raw()</code> com concatenação de strings.</p>

<h2>Boas Práticas e Validação Complementar</h2>

<p>Prepared Statements resolvem SQL Injection, mas não são a única camada de defesa. Validação de entrada e sanitização de saída complementam a segurança.</p>

<h3>Validação de Entrada</h3>

<p>Sempre valide o tipo, tamanho e formato esperado. Se espera um email, rejeite valores que não sejam emails. Se espera um inteiro, converta e valide antes de usar.</p>

<pre><code class="language-python">import re

from datetime import datetime

def validar_email(email):

padrao = r&#039;^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$&#039;

return re.match(padrao, email) is not None

def validar_idade(idade_str):

try:

idade = int(idade_str)

return 0 &lt;= idade &lt;= 150

except ValueError:

return False

email = input(&quot;Email: &quot;)

if not validar_email(email):

print(&quot;Email inválido!&quot;)

exit()

idade = input(&quot;Idade: &quot;)

if not validar_idade(idade):

print(&quot;Idade inválida!&quot;)

exit()

Agora é seguro usar em prepared statement

cursor.execute(&quot;INSERT INTO clientes (email, idade) VALUES (?, ?)&quot;, (email, int(idade)))</code></pre>

<h3>Sanitização de Saída</h3>

<p>Dados exibidos ao usuário podem conter HTML/JavaScript malicioso (XSS). Use funções de escape:</p>

<pre><code class="language-php">&lt;?php

$usuario = &quot;João &lt;script&gt;alert(&#039;xss&#039;)&lt;/script&gt;&quot;;

// XSS Prevention

echo htmlspecialchars($usuario, ENT_QUOTES, &#039;UTF-8&#039;);

// Output: João &amp;lt;script&amp;gt;alert(&#039;xss&#039;)&amp;lt;/script&amp;gt;

?&gt;</code></pre>

<h3>Princípio do Menor Privilégio</h3>

<p>Use contas de banco de dados com permissões mínimas. O usuário da aplicação não precisa de acesso <code>DROP TABLE</code>.</p>

<pre><code class="language-sql">-- Cria usuário apenas para SELECT, INSERT, UPDATE em tabelas específicas

CREATE USER &#039;app_user&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;senha_forte&#039;;

GRANT SELECT, INSERT, UPDATE ON database.usuarios TO &#039;app_user&#039;@&#039;localhost&#039;;

-- Nunca concede DELETE ou DROP</code></pre>

<h2>Conclusão</h2>

<p><strong>Prepared Statements são obrigatórios</strong> para qualquer aplicação que acesse banco de dados. Eles eliminam a raiz do SQL Injection ao separar lógica de dados. Nenhuma quantidade de validação manual substitui prepared statements — use-os em 100% das queries que envolvem entrada de usuário. Combine com validação de entrada, sanitização de saída e princípio do menor privilégio para uma defesa em profundidade. Negligenciar isso é negligenciar a segurança dos seus usuários.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://owasp.org/Top10/A03_2021-Injection/" target="_blank" rel="noopener noreferrer">OWASP Top 10 - A03:2021 Injection</a></li>

<li><a href="https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php" target="_blank" rel="noopener noreferrer">PHP: Prepared Statements (MySQLi)</a></li>

<li><a href="https://docs.python.org/3/library/sqlite3.html" target="_blank" rel="noopener noreferrer">Python sqlite3 Documentation</a></li>

<li><a href="https://www.postgresql.org/docs/current/sql-prepare.html" target="_blank" rel="noopener noreferrer">PostgreSQL: Prepared Statements</a></li>

<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html" target="_blank" rel="noopener noreferrer">OWASP: SQL Injection Prevention Cheat Sheet</a></li>

</ul>

Comentários

Mais em PHP

Segurança em PHP: XSS, CSRF, Injeção e Boas Práticas Finais: Do Básico ao Avançado
Segurança em PHP: XSS, CSRF, Injeção e Boas Práticas Finais: Do Básico ao Avançado

XSS (Cross-Site Scripting) XSS ocorre quando um atacante injeta código JavaSc...

Boas Práticas de Namespaces e Autoloading com PSR-4 para Times Ágeis
Boas Práticas de Namespaces e Autoloading com PSR-4 para Times Ágeis

O que são Namespaces? Namespaces são mecanismos de organização de código que...

Guia Completo de Interfaces e Classes Abstratas em PHP
Guia Completo de Interfaces e Classes Abstratas em PHP

Classes Abstratas em PHP Uma classe abstrata é um molde que não pode ser inst...