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
$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
// 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
}
?>
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
// 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) |
3. Injeção de SQL — Demonstração ao vivo
▶ Exemplo: Ataque de injeção de SQL
<?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!
?>
▶ Exemplo: Instruções preparadas seguras evitam injeções
<?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
?>
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
// 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
// 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
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). Ohtmlspecialcharsevita o XSS (camada de saída, Lição 18). Essas são linhas de defesa em camadas diferentes — você precisa das duas.
P:
password_hashgera resultados diferentes a cada vez — como funciona a verificação? R: O valor de hash inclui um “salt” gerado aleatoriamente.password_verifyextrai 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
bindValueatribui um valor imediatamente;bindParamatribui uma referência a uma variável (valor lido no momento da execução)- Transações:
beginTransaction → execute → commitourollBack - Demonstração de injeção de SQL:
' OR '1'='1' --→ sempre use instruções preparadas password_hash()para cadastro,password_verify()para verificação de loginpassword_needs_rehash()verifica se o algoritmo de hash precisa ser atualizado- As instruções preparadas evitam a injeção de SQL ≠ evitam o XSS — ambas as linhas de defesa são essenciais
📝 Exercícios
- 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).
- 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. - Crie um sistema completo de cadastro e login: use
password_hashpara o cadastro,password_verifypara o login e adicione a detecção depassword_needs_rehash.



