PDO avançado e segurança

Na aula anterior, você aprendeu os conceitos básicos de CRUD no PDO. Esta aula vai um passo além: as transações garantem que suas operações sejam do tipo “tudo ou nada”, o hash de senhas mantém seu banco de dados seguro mesmo em caso de vazamento, e ainda teremos uma demonstração ao vivo de injeção de SQL que você nunca vai esquecer.

1. bindValue x bindParam

PHP
<?php
$stmt = $pdo->prepare("INSERT INTO users (username, age) VALUES (:name, :age)");

// bindValue: the value is determined at binding time (pass by value)
$name = 'John';
$stmt->bindValue(':name', $name, PDO::PARAM_STR);

// bindParam: binds a variable reference—the value is read at execute() time
$age = 25;
$stmt->bindParam(':age', $age, PDO::PARAM_INT);

// Key difference:
$name = 'Jane';  // bindValue already bound "John"—changing $name has no effect
$age = 30;        // bindParam references $age—execute() will use 30

$stmt->execute();  // INSERT: name='John', age=30
?>
bindValue bindParam
Momento da vinculação O valor é definido imediatamente na chamada O valor é lido no momento da execução de execute()
Parâmetro Pode ser um literal ou uma expressão Deve ser uma variável
Ideal para A maioria dos cenários Ao executar a mesma instrução em um loop
PHP
<?php
// bindParam shines in loops
$stmt = $pdo->prepare("INSERT INTO logs (message) VALUES (:msg)");
$stmt->bindParam(':msg', $msg);

foreach (['Service started', 'User logged in', 'Request processed', 'Service stopped'] as $msg) {
    $stmt->execute();  // Each execute() picks up the current value of $msg
}
?>
💡 Dica: Na maioria dos casos, execute(['key' => $val]) é suficiente — você não precisa de bindValue/bindParam. Use bind apenas quando precisar especificar explicitamente os tipos (PDO::PARAM_INT / PDO::PARAM_BOOL) ou ao executar em um loop.


2. Transações

Um cenário de transferência: o saldo de A diminui, o saldo de B aumenta — ambas as operações devem ser bem-sucedidas juntas ou falhar juntas:

PHP
<?php
// Create a test table
// CREATE TABLE accounts (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), balance DECIMAL(10,2));

try {
    $pdo->beginTransaction();
    
    // John's account: subtract 200
    $stmt = $pdo->prepare("UPDATE accounts SET balance = balance - :amount WHERE name = :name");
    $stmt->execute(['amount' => 200, 'name' => 'John']);
    
    // Jane's account: add 200
    $stmt->execute(['amount' => 200, 'name' => 'Jane']);
    
    $pdo->commit();
    echo "Transfer successful! John → Jane: 200";
} catch (Exception $e) {
    $pdo->rollBack();
    echo "Transfer failed, rolled back: " . $e->getMessage();
}
?>
Método Ação
beginTransaction() Iniciar uma transação
commit() Confirmar (confirmar todas as operações)
rollBack() Reverter (desfazer todas as operações)
💡 Dica: Lembre-se dos quatro princípios das transações: tudo ou nada (Atomicidade), os dados permanecem consistentes (Consistência), as operações simultâneas não interferem entre si (Isolamento) e, uma vez confirmados, os dados persistem (Durabilidade). Juntos: ACID.


3. Injeção de SQL — Demonstração ao vivo

▶ Exemplo: Ataque de injeção de SQL

PHP
<?php
// Imagine a login check (❌ Dangerous pattern—never use)
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';

// ❌ Fatal mistake: directly concatenating SQL
$sql = "SELECT * FROM users WHERE username = '{$username}' AND password = '{$password}'";
echo "Executing SQL: {$sql}<br>";

// An attacker types this into the username field:
// ' OR '1'='1' --
// The resulting SQL becomes:
//   SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = ''
//   1=1 is always true! -- comments out the rest! All users are returned!

// An even more terrifying attack:
// '; DROP TABLE users; --
// The resulting SQL:
//   SELECT * FROM users WHERE username = ''; DROP TABLE users; --' AND password = ''
//   The entire table is deleted!
?>
▶ Experimente

▶ Exemplo: Instruções preparadas seguras evitam injeções

PHP
<?php
// ✅ Safe approach—use prepared statements
$sql = "SELECT * FROM users WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($sql);
$stmt->execute([
    'username' => $_POST['username'] ?? '',
    'password' => $_POST['password'] ?? '',
]);
$user = $stmt->fetch();

if ($user) {
    echo "Login successful!";
} else {
    echo "Invalid username or password";
}
// The attacker's input ' OR '1'='1' -- is treated as a plain string to match, not executed
?>
▶ Experimente
🔥 Erro comum: A injeção de SQL não é coisa do passado — o relatório global de segurança na web da OWASP ainda a lista como o risco de segurança número 1. Lembre-se da regra de ouro dos dados: nunca confie nas entradas do usuário; use sempre instruções preparadas.


4. Hash de senha

Armazenar senhas em texto simples = suicídio. O hash de senhas mantém seu banco de dados seguro, mesmo que haja um vazamento:

PHP
<?php
// Registration: hash the password
$password = "mySecret123";
$hash = password_hash($password, PASSWORD_DEFAULT);
echo $hash;  
// $2y$10$WzjCuq4qY8yXqUqMqHqRdOq... 
// Each hash result is different (because of a random salt)

// Store in the database
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (:u, :p)");
$stmt->execute(['u' => 'John', 'p' => $hash]);

// Login: verify the password
$inputPassword = "mySecret123";
$stmt = $pdo->prepare("SELECT password FROM users WHERE username = :u");
$stmt->execute(['u' => 'John']);
$user = $stmt->fetch();

if ($user && password_verify($inputPassword, $user['password'])) {
    echo "✅ Password correct, login successful!";
} else {
    echo "❌ Incorrect password";
}
// Even if someone gets the database, they only see hash values—they can't log in directly
?>
Algoritmo Descrição Recomendado
PASSWORD_DEFAULT bcrypt (padrão atual) ✅ Recomendado
PASSWORD_BCRYPT bcrypt explícito
PASSWORD_ARGON2I Argon2 (PHP 7.2+) ✅ Mais recente, mais robusto
PASSWORD_ARGON2ID Argon2id (PHP 7.3+) ✅ Mais recente, mais forte
PHP
<?php
// Advanced: increase the cost factor
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
// Higher cost is more secure but slower (10-12 is recommended)

// Check if a hash needs rehashing (when algorithms are upgraded)
if (password_needs_rehash($hash, PASSWORD_DEFAULT, ['cost' => 12])) {
    $newHash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
    // Update the hash in the database
}
?>

5. Modelo de código de segurança

PHP
<?php
class Auth {
    private PDO $pdo;
    
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    
    /**
     * User registration
     */
    public function register(string $username, string $email, string $password): array {
        // Check if the username already exists
        $stmt = $this->pdo->prepare("SELECT COUNT(*) FROM users WHERE username = :u");
        $stmt->execute(['u' => $username]);
        if ($stmt->fetchColumn() > 0) {
            return ['success' => false, 'message' => 'Username already taken'];
        }
        
        // Insert the new user
        $hash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
        $stmt = $this->pdo->prepare(
            "INSERT INTO users (username, email, password) VALUES (:u, :e, :p)"
        );
        $stmt->execute(['u' => $username, 'e' => $email, 'p' => $hash]);
        
        return ['success' => true, 'id' => $this->pdo->lastInsertId()];
    }
    
    /**
     * User login
     */
    public function login(string $username, string $password): array {
        $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = :u");
        $stmt->execute(['u' => $username]);
        $user = $stmt->fetch();
        
        if (!$user || !password_verify($password, $user['password'])) {
            return ['success' => false, 'message' => 'Invalid username or password'];
        }
        
        // Check if the hash needs updating
        if (password_needs_rehash($user['password'], PASSWORD_DEFAULT, ['cost' => 12])) {
            $newHash = password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
            $stmt = $this->pdo->prepare("UPDATE users SET password = :p WHERE id = :id");
            $stmt->execute(['p' => $newHash, 'id' => $user['id']]);
        }
        
        unset($user['password']);  // Don't store the password hash in the session
        return ['success' => true, 'user' => $user];
    }
}

// Usage
$auth = new Auth($pdo);

// Register
print_r($auth->register('TestUser', 'test@example.com', 'StrongPass123'));

// Login
print_r($auth->login('TestUser', 'StrongPass123'));
?>

❓ Perguntas Frequentes

P: As instruções preparadas já oferecem uma camada de proteção — ainda preciso do htmlspecialchars? R: Sim! As instruções preparadas evitam a injeção de SQL (camada do banco de dados). O htmlspecialchars evita o XSS (camada de saída, Lição 18). Essas são linhas de defesa em camadas diferentes — você precisa das duas.

P: password_hash gera resultados diferentes a cada vez — como funciona a verificação? R: O valor de hash inclui um “salt” gerado aleatoriamente. password_verify extrai o salt do hash, calcula o hash da senha de entrada com o mesmo salt e compara os resultados. Portanto, cada resultado de hash é diferente, mas a verificação sempre funciona.

P: E se algumas operações em uma transação forem bem-sucedidas e outras falharem? R: “Metade bem-sucedida, metade falhada” não pode acontecer — é exatamente isso que as transações evitam. Ou tudo entra em vigor após o commit, ou nada entra em vigor após o rollback. Os dados só são gravados permanentemente após um commit bem-sucedido.

📖 Resumo

📝 Exercícios

  1. Escreva um sistema de transferência simples: crie uma tabela de contas, use transações para transferir de A para B (atualize ambos os saldos simultaneamente — reverta a operação se alguma etapa falhar).
  2. Crie propositalmente um formulário de login sem instruções preparadas. Digite ' OR '1'='1' -- para observar o efeito da injeção de SQL. Em seguida, refatore-o utilizando instruções preparadas e perceba a melhoria na segurança.
  3. Crie um sistema completo de cadastro e login: use password_hash para o cadastro, password_verify para o login e adicione a detecção de password_needs_rehash.
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%