Artigo 46 — Segurança Avançada em PHP
Módulo 8 · Semana 46 · Nível: Avançado
Introdução
O artigo 37 cobriu os vetores mais comuns — XSS, CSRF e SQL Injection. Segurança avançada vai além: envolve criptografia correta, proteção de dados sensíveis, headers HTTP defensivos, auditoria de dependências, e a mentalidade de tratar segurança não como checklist, mas como camada contínua do processo de desenvolvimento.
Um sistema seguro não é aquele que nunca foi atacado. É aquele que, quando atacado, limita o dano ao mínimo possível.
Criptografia simétrica com OpenSSL
Para criptografar dados sensíveis no banco (números de cartão, documentos, dados médicos), use criptografia simétrica com AES-256-GCM — o modo mais seguro disponível:
<?php
declare(strict_types=1);
final class Criptografia
{
private const ALGORITMO = 'aes-256-gcm';
private const TAG_LENGTH = 16;
public function __construct(
private readonly string $chave // 32 bytes — gerado com random_bytes(32)
) {
if (strlen($this->chave) !== 32) {
throw new \InvalidArgumentException('Chave deve ter exatamente 32 bytes.');
}
}
public function criptografar(string $texto): string
{
$iv = random_bytes(openssl_cipher_iv_length(self::ALGORITMO));
$tag = '';
$cifrado = openssl_encrypt(
data: $texto,
cipher_algo: self::ALGORITMO,
passphrase: $this->chave,
options: OPENSSL_RAW_DATA,
iv: $iv,
tag: $tag,
tag_length: self::TAG_LENGTH,
);
if ($cifrado === false) {
throw new \RuntimeException('Falha na criptografia.');
}
// Empacota: iv + tag + cifrado, tudo em base64
return base64_encode($iv . $tag . $cifrado);
}
public function descriptografar(string $pacote): string
{
$dados = base64_decode($pacote, strict: true);
if ($dados === false) {
throw new \RuntimeException('Pacote inválido.');
}
$ivLen = openssl_cipher_iv_length(self::ALGORITMO);
$iv = substr($dados, 0, $ivLen);
$tag = substr($dados, $ivLen, self::TAG_LENGTH);
$cifrado = substr($dados, $ivLen + self::TAG_LENGTH);
$texto = openssl_decrypt(
data: $cifrado,
cipher_algo: self::ALGORITMO,
passphrase: $this->chave,
options: OPENSSL_RAW_DATA,
iv: $iv,
tag: $tag,
);
if ($texto === false) {
throw new \RuntimeException('Falha na descriptografia — dados corrompidos ou chave incorreta.');
}
return $texto;
}
}
// Uso:
$chave = base64_decode($_ENV['ENCRYPTION_KEY']); // 32 bytes armazenados em base64 no .env
$cripto = new Criptografia($chave);
$cifrado = $cripto->criptografar('4111111111111111'); // número de cartão
$descriptografado = $cripto->descriptografar($cifrado);
Gerar e armazenar a chave com segurança:
# Gera uma chave de 32 bytes em base64 — armazene no .env, nunca no código
php -r "echo base64_encode(random_bytes(32)) . PHP_EOL;"
Criptografia assimétrica para dados entre sistemas
Quando dois sistemas precisam trocar dados sensíveis, use RSA ou Ed25519:
<?php
// Gerar par de chaves
$config = [
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'private_key_bits' => 4096,
'digest_alg' => 'sha256',
];
$recurso = openssl_pkey_new($config);
$chavePriv = '';
openssl_pkey_export($recurso, $chavePriv);
$detalhes = openssl_pkey_get_details($recurso);
$chavePub = $detalhes['key'];
// Criptografar com chave pública (apenas quem tem a privada pode descriptografar)
openssl_public_encrypt('dados sensíveis', $cifrado, $chavePub);
$cifradoB64 = base64_encode($cifrado);
// Descriptografar com chave privada
openssl_private_decrypt(base64_decode($cifradoB64), $texto, $chavePriv);
// Assinar com chave privada (prova de autoria)
openssl_sign('mensagem', $assinatura, $chavePriv, OPENSSL_ALGO_SHA256);
// Verificar assinatura com chave pública
$valido = openssl_verify('mensagem', $assinatura, $chavePub, OPENSSL_ALGO_SHA256) === 1;
Proteção de dados pessoais — LGPD na prática
A Lei Geral de Proteção de Dados exige que dados pessoais sejam coletados minimamente, protegidos adequadamente, e que usuários possam solicitar exclusão:
<?php
declare(strict_types=1);
class RepositorioDeUsuario
{
// Criptografa dados sensíveis antes de salvar
public function salvar(DadosDeUsuario $dados, Criptografia $cripto): int
{
$stmt = $this->pdo->prepare("
INSERT INTO usuarios (nome, email, cpf_criptografado, telefone_criptografado)
VALUES (?, ?, ?, ?)
");
$stmt->execute([
$dados->nome,
$dados->email,
$cripto->criptografar($dados->cpf), // PII criptografado em repouso
$cripto->criptografar($dados->telefone),
]);
return (int) $this->pdo->lastInsertId();
}
// Direito ao esquecimento — anonimização
public function anonimizar(int $usuarioId): void
{
$this->pdo->prepare("
UPDATE usuarios SET
nome = 'Usuário Removido',
email = CONCAT('removido_', id, '@anonimo.local'),
cpf_criptografado = NULL,
telefone_criptografado = NULL,
anonimizado_em = NOW()
WHERE id = ?
")->execute([$usuarioId]);
}
// Log de acesso a dados pessoais (trilha de auditoria)
public function registrarAcesso(int $usuarioId, int $acessadoPor, string $motivo): void
{
$this->pdo->prepare("
INSERT INTO log_acesso_pii (usuario_id, acessado_por, motivo, ip, criado_em)
VALUES (?, ?, ?, ?, NOW())
")->execute([$usuarioId, $acessadoPor, $motivo, $_SERVER['REMOTE_ADDR'] ?? '']);
}
}
Headers HTTP de segurança — configuração completa
<?php
declare(strict_types=1);
final class HeadersDeSeguranca
{
public static function aplicar(): void
{
// Impede MIME sniffing — o browser respeita o Content-Type declarado
header('X-Content-Type-Options: nosniff');
// Impede clickjacking — a página não pode ser carregada em iframe de outro domínio
header('X-Frame-Options: DENY');
// Força HTTPS por 1 ano (incluindo subdomínios)
header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
// Controla informações enviadas no Referer
header('Referrer-Policy: strict-origin-when-cross-origin');
// Desativa recursos do browser que a aplicação não usa
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()');
// Content Security Policy — política granular de fontes permitidas
$csp = implode('; ', [
"default-src 'self'",
"script-src 'self' https://cdn.jsdelivr.net",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
]);
header("Content-Security-Policy: {$csp}");
}
}
No Laravel, centralize isso em um middleware:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class HeadersDeSeguranca
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
return $response;
}
}
Auditoria de dependências
Dependências desatualizadas são um vetor frequente de ataque. Automatize a verificação:
# Verifica vulnerabilidades conhecidas nas dependências instaladas
composer audit
# Saída de exemplo:
# Found 1 security vulnerability advisory affecting 1 package:
# Package: vendor/pacote
# CVE: CVE-2024-XXXX
# Title: Remote Code Execution via unsanitized input
# URL: https://github.com/...
# Atualiza para versões seguras
composer update vendor/pacote
# Verificar dependências desatualizadas
composer outdated
No CI/CD, adicione composer audit como etapa obrigatória — o build falha se houver vulnerabilidades:
# .github/workflows/security.yml
- name: Auditoria de segurança
run: composer audit --no-dev
Timing attacks e comparações seguras
<?php
// VULNERÁVEL a timing attack — a comparação para no primeiro caractere diferente
if ($tokenEnviado === $tokenArmazenado) { }
// CORRETO — hash_equals tem tempo constante independente de onde diverge
if (!hash_equals($tokenArmazenado, $tokenEnviado)) {
throw new \RuntimeException('Token inválido.');
}
// O mesmo princípio se aplica a qualquer comparação de segredo:
// API keys, tokens de webhook, assinaturas HMAC
$assinatura = hash_hmac('sha256', $payload, $_ENV['WEBHOOK_SECRET']);
$assinaturaOk = hash_equals($assinatura, $assinaturaRecebida);
Proteção contra enumeração de usuários
Um erro clássico é dar respostas diferentes para "email não existe" e "senha incorreta" — isso permite que atacantes descubram quais emails estão cadastrados:
<?php
// RUIM — revela se o email existe
public function login(string $email, string $senha): bool
{
$usuario = $this->repositorio->buscarPorEmail($email);
if (!$usuario) {
throw new \Exception('Email não cadastrado.'); // vaza informação
}
if (!password_verify($senha, $usuario->senhaHash)) {
throw new \Exception('Senha incorreta.'); // também vaza
}
return true;
}
// BOM — mesma mensagem para ambos os casos
public function login(string $email, string $senha): bool
{
$usuario = $this->repositorio->buscarPorEmail($email);
// Executa password_verify mesmo quando o usuário não existe
// para manter tempo de resposta constante (evita timing attack por ausência)
$hashFalso = '$argon2id$v=19$m=65536,t=4,p=1$...'; // hash inválido mas bem formado
$hashVerif = $usuario?->senhaHash ?? $hashFalso;
if (!$usuario || !password_verify($senha, $hashVerif)) {
throw new \RuntimeException('Credenciais inválidas.'); // mensagem genérica
}
return true;
}
Resumo
| Técnica | Proteção |
|---|---|
| AES-256-GCM | Dados sensíveis em repouso |
| RSA / Ed25519 | Comunicação entre sistemas |
| Headers HTTP | XSS, clickjacking, MITM |
hash_equals() |
Timing attacks em comparações de segredos |
composer audit |
Vulnerabilidades em dependências |
| Mensagens genéricas de login | Enumeração de usuários |
| Anonimização | Conformidade com LGPD/GDPR |
Exercício da semana
-
Implemente a classe
Criptografiacom AES-256-GCM. Criptografe CPF e telefone de usuários no banco. Garanta que a descriptografia funciona e que dados corrompidos lançam exceção. -
Adicione o middleware de headers de segurança ao Laravel. Valide os headers com a ferramenta https://securityheaders.com/ (use ngrok ou similar para expor localmente).
-
Configure
composer auditno pipeline de CI. Simule uma vulnerabilidade instalando uma versão antiga de um pacote e confirme que o build falha. -
Refatore o sistema de login do artigo 35 para usar mensagem genérica de erro. Implemente o hash falso para garantir tempo de resposta constante.
-
Desafio: implemente assinatura de webhook: ao receber um POST de um sistema externo, valide a assinatura HMAC-SHA256 no header
X-Signature. Rejeite requisições com assinatura inválida ou mais antigas que 5 minutos (use timestamp no payload).
Referências
- OWASP Cryptographic Storage Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html
- PHP Manual — OpenSSL: https://www.php.net/manual/pt_BR/book.openssl.php
- LGPD — Lei 13.709/2018: https://www.planalto.gov.br/ccivil_03/_ato2015-2018/2018/lei/l13709.htm
- Security Headers: https://securityheaders.com/