PHP

Artigo 46 — Segurança Avançada em PHP Já leu

8 min de leitura

Artigo 46 — Segurança Avançada em PHP
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,

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

  1. Implemente a classe Criptografia com 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.

  2. 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).

  3. Configure composer audit no pipeline de CI. Simule uma vulnerabilidade instalando uma versão antiga de um pacote e confirme que o build falha.

  4. 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.

  5. 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/

Comentários

Mais em PHP

Artigo 47 — Testes Automatizados com PHPUnit e Pest
Artigo 47 — Testes Automatizados com PHPUnit e Pest

Testes automatizados são o seguro de vida do software. Sem eles, cada mudança...

PostgreSQL com PHP
PostgreSQL com PHP

O MySQL é o banco de dados mais comum no ecossistema PHP, mas o PostgreSQL é...

Artigo 51 — Projeto Final Parte 2: Implementação
Artigo 51 — Projeto Final Parte 2: Implementação

Com o planejamento sólido do artigo anterior, agora implementamos o sistema c...