Parte 07

Segurança

As ameaças mais comuns em sistemas web e como se defender de cada uma — com exemplos práticos em PHP. Regra mestre: nunca confie no que vem do cliente (formulário, URL, header, cookie, arquivo).

📌 Defesa em profundidade: nenhuma camada sozinha basta. Validação no servidor + escape na saída + prepared statements + HTTPS + WAF se somam. Frontend é só UX — o atacante manda requisição direto, sem passar pela sua tela.
1

XSS & HTML injection

XSS (Cross-Site Scripting) é quando um atacante injeta HTML/JS numa página que outros vão ver. Ex.: um comentário com <script>roubaCookie()</script>. A defesa é escapar toda saída que vem de usuário/banco.

escape anti-XSS
function h(string $s): string {
    return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
}

// ERRADO: imprime o que o usuário mandou, do jeito que veio.
echo "<p>" . $comentario . "</p>";

// CERTO: escapa < > & " ' antes de imprimir.
echo "<p>" . h($comentario) . "</p>";

Uma segunda camada é o header CSP (Content-Security-Policy), que diz ao navegador de onde scripts podem rodar:

CSP (no topo do PHP)
header("Content-Security-Policy: default-src 'self'");   // só scripts do próprio domínio
header("X-Content-Type-Options: nosniff");
⚠️ O contexto importa: htmlspecialchars serve pra HTML. Dentro de um atributo, de uma URL (href) ou de <script> as regras mudam — nunca jogue dado de usuário cru dentro de onclick, href="javascript:..." ou de um bloco <script>.
2

SQL injection

Acontece quando você concatena input dentro da query. Um email com ' OR '1'='1 vira um login que sempre passa. A defesa é prepared statement: a query vai com ? e os valores separados — o banco trata como dado, nunca como comando.

prepared statement (PDO)
// ERRADO: input direto na string -> injeção.
$pdo->query("SELECT * FROM users WHERE email = '" . $email . "'");

// CERTO: ? no lugar do valor; valor vai no execute().
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
📌 Já aplicado: todo o CRUD de pacientes e o mini-blog usam prepared statements. Regra: se há valor de fora na query, sempre prepare() + execute().
3

CSRF (Cross-Site Request Forgery)

Um site malicioso faz o navegador do usuário logado enviar uma requisição pro seu site (ex.: um <form> escondido que transfere dinheiro). Como o cookie de sessão vai junto, o servidor acha que foi o usuário. A defesa é um token secreto em cada formulário, que o site atacante não tem como adivinhar.

token CSRF
session_start();

// Gera o token uma vez por sessão (??= só cria se ainda não existe).
$_SESSION["csrf"] ??= bin2hex(random_bytes(32));

// No formulário:
// <input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">

// Ao receber o POST, confere ANTES de tudo:
if (!hash_equals($_SESSION["csrf"], $_POST["csrf"] ?? "")) {
    http_response_code(419);
    exit("Requisição inválida (CSRF).");
}
⚠️ Detalhes que importam: use hash_equals (compara em tempo constante, contra timing attack), nunca ==. Reforce com cookie de sessão SameSite=Lax. E lembre: nunca altere dado por GET — só POST (foi a correção que fizemos no pacientes.php).
4

Hash de senhas

Senha nunca é guardada em texto, nem com md5/sha1 (quebráveis). Use password_hash(), que aplica bcrypt/argon com salt automático. Pra conferir, password_verify() — você nunca "descriptografa", só compara.

cadastro e login
// --- No CADASTRO: guarda o hash, não a senha. ---
$hash = password_hash($senha, PASSWORD_DEFAULT);
// $hash -> algo como "$2y$10$abc..." (60 chars). Salve ISSO no banco.

// --- No LOGIN: compara a digitada com o hash salvo. ---
if (password_verify($senhaDigitada, $hashDoBanco)) {
    // senha confere -> cria a sessão
    $_SESSION["user_id"] = $user["id"];
} else {
    echo "E-mail ou senha incorretos.";   // msg genérica (não diga qual errou)
}
📌 Boas práticas: a coluna do banco deve ter VARCHAR(255) (o hash cresce com o tempo). Mensagem de erro genérica no login (não revele se o e-mail existe). E rate limit no login (seção 8) contra força bruta.
5

Upload malicioso

O perigo: alguém sobe shell.php disfarçado e depois o executa pela URL. Regras: confira o MIME real (não o enviado), regere o nome, force a extensão pelo tipo, limite o tamanho e salve onde o PHP não roda.

upload seguro de imagem
$f = $_FILES["arquivo"];

// 1) Tamanho máximo (ex.: 2 MB).
if ($f["size"] > 2 * 1024 * 1024) exit("Arquivo grande demais.");

// 2) MIME REAL (lê o conteúdo; não confie em $f["type"], que o cliente forja).
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($f["tmp_name"]);
$permitidos = ["image/jpeg" => "jpg", "image/png" => "png"];
if (!isset($permitidos[$mime])) exit("Tipo não permitido.");

// 3) Nome aleatório + extensão definida pelo SERVIDOR (não pelo upload).
$nome = bin2hex(random_bytes(16)) . "." . $permitidos[$mime];

// 4) Salva numa pasta de uploads (idealmente fora do web root).
move_uploaded_file($f["tmp_name"], __DIR__ . "/../uploads/" . $nome);
⚠️ A armadilha clássica: confiar em $_FILES["arquivo"]["type"] ou na extensão do nome enviado — os dois são controlados pelo cliente. Sempre cheque o MIME real com finfo e regere o nome. Na pasta de uploads, desligue execução de PHP (via .htaccess).
6

APIs & JWT

APIs não têm sessão por cookie como o site. O padrão é o JWT (JSON Web Token): o servidor entrega um token assinado no login, e o cliente o manda em cada requisição no header Authorization. O servidor confere a assinatura — sem precisar guardar nada.

anatomia de um JWT
  HEADER          .        PAYLOAD             .      SIGNATURE
{"alg":"HS256"}   .  {"sub":123,"exp":1700}   .   HMAC(header.payload, segredo)
   base64url             base64url                    prova de autenticidade

Authorization: Bearer eyJhbGci...  <- o cliente manda assim em cada request

Na prática se usa uma lib (firebase/php-jwt via Composer), mas o conceito de validar é:

validar token (conceito)
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

// Pega o token do header Authorization.
$token = str_replace("Bearer ", "", $_SERVER["HTTP_AUTHORIZATION"] ?? "");

try {
    // decode confere a assinatura E a expiração; lança exceção se inválido.
    $dados = JWT::decode($token, new Key($segredo, "HS256"));
    $userId = $dados->sub;
} catch (\Exception $e) {
    http_response_code(401);
    exit(json_encode(["erro" => "Token inválido"]));
}
⚠️ Armadilhas de JWT: o payload é só base64, não criptografia — qualquer um lê! Nunca bote senha ou dado sensível nele. Sempre defina exp (expiração curta). Valide a assinatura SEMPRE. Use um $segredo forte e fora do código (variável de ambiente).
7

CORS (Cross-Origin Resource Sharing)

Por padrão, o navegador bloqueia uma página em siteA.com de chamar uma API em siteB.com. O CORS é o servidor da API autorizando origens específicas, via headers de resposta.

headers CORS na API
// Libera SÓ o seu front (específico, não "*").
header("Access-Control-Allow-Origin: https://meuapp.com");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");

// O navegador manda um "preflight" OPTIONS antes do POST real:
if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") {
    http_response_code(204);
    exit;
}
⚠️ Entenda o que CORS NÃO é: não é proteção do seu servidor — é uma regra do navegador. Um atacante com curl ignora CORS totalmente. Por isso a API ainda precisa de autenticação (JWT) e validação. E Allow-Origin: * não combina com credenciais/cookies.
8

Rate limit

Limitar quantas requisições alguém faz num intervalo. Essencial no login (contra força bruta) e em APIs. Devolve HTTP 429 (Too Many Requests) quando estoura.

limite simples de tentativas
session_start();

// Conta tentativas de login nesta sessão (exemplo didático).
$_SESSION["tentativas"] = ($_SESSION["tentativas"] ?? 0) + 1;

if ($_SESSION["tentativas"] > 5) {
    http_response_code(429);
    exit("Muitas tentativas. Tente de novo em alguns minutos.");
}

// ...verifica a senha; se acertar, zere: $_SESSION["tentativas"] = 0;
📌 Em produção: o exemplo acima é por sessão (fácil de burlar limpando cookie). O correto é contar por IP + janela de tempo num armazenamento compartilhado (Redis/banco), ou deixar o Cloudflare/WAF (seção 9) fazer isso na borda, antes de chegar no PHP.
9

Cloudflare & WAF

Cloudflare (e WAFs em geral) ficam na frente do seu servidor, como um porteiro. Toda requisição passa por ele antes de chegar no seu PHP — e ele filtra ataques, distribui carga e esconde o IP de origem.

FunçãoO que faz
CDNserve estáticos (css/img) de servidores perto do usuário → mais rápido
Anti-DDoSabsorve enxurradas de tráfego malicioso antes de derrubar seu servidor
WAFregras que bloqueiam padrões de ataque (SQLi, XSS) na borda
Rate limit / Botdesafia visitantes suspeitos (o "verificando seu navegador...")
SSLHTTPS automático entre o visitante e o Cloudflare
📌 Você já viu isso: quando publicamos o blog, o curl batia numa tela "One moment, please..." — era exatamente um desafio anti-bot do servidor (estilo Cloudflare). Um navegador real passa; scripts automatizados não.
⚠️ WAF não substitui código seguro: é uma camada extra (defesa em profundidade), não um passe livre. Um app com SQL injection continua vulnerável a quem passar pelo WAF. As seções 1–8 continuam obrigatórias — o Cloudflare é o reforço, não a base.

Resumo: ameaça → defesa

AmeaçaDefesa principalFunção/header chave
XSSescapar saída + CSPhtmlspecialchars
SQL injectionprepared statementsprepare + execute
CSRFtoken na sessãohash_equals
Senha vazadahash fortepassword_hash
Upload maliciosoMIME real + nome regeradofinfo
Token forjado (API)JWT assinado + expJWT::decode
Origem não autorizadaCORS específicoAccess-Control-Allow-Origin
Força brutarate limitHTTP 429
DDoS / botsWAF na bordaCloudflare
← Parte 6 · Arquitetura Voltar à capa →