Segurança web
Injeção SQL, XSS, CSRF, travessia de caminho, HTTPS e guarda de palavras passe, com correção guiada.
Nesta página
Todas as páginas que escreveste até aqui confiam no utilizador: mostram o que ele escreve, executam o que ele envia, abrem o ficheiro que ele pede. A segurança web é inverter essa confiança por omissão. Cada ataque desta página explora um sítio onde o programador misturou dados com instruções; cada defesa volta a separá-los. Vê cada par antes e depois com os teus olhos antes de leres a explicação.
Antes, com o parâmetro colado no SQL:
$sql = "SELECT nome, preco FROM produtos WHERE nome LIKE '%$termo%'";Depois, com o marcador a separar dado de instrução:
$stmt = $db->prepare('SELECT nome, preco FROM produtos WHERE nome LIKE :t');
$stmt->execute([':t' => '%' . $termo . '%']);Antes, com a saída crua a executar o que o utilizador escreveu:
echo '<p>' . $comentario . '</p>';Depois, com o escape a transformar etiquetas em texto visível:
echo '<p>' . htmlspecialchars($comentario) . '</p>';Antes, sem segredo que o atacante desconheça:
<form method="post">
<input type="hidden" name="acao" value="apagar" />
<button>Apagar conta</button>
</form>Depois, com o token que só a tua página conhece:
<form method="post">
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>" />
<input type="hidden" name="acao" value="apagar" />
<button>Apagar conta</button>
</form>Injeção SQL: nunca colar entrada no SQL
A página de pesquisa do PHP já usa a defesa certa, por isso vê primeiro o ataque contra a versão ingénua. Se o código colar o parâmetro no SQL:
$sql = "SELECT nome, preco FROM produtos WHERE nome LIKE '%$termo%'";
então pesquisar ' OR '1'='1 transforma a consulta em algo que casa com todas as linhas, e pesquisar com ponto e vírgula pode empilhar comandos destrutivos. A correção é o prepare com marcadores, que já usas: o termo viaja como dado e nunca é interpretado como SQL1. Regra sem exceções: nenhum $_GET, $_POST ou cookie entra em texto de SQL. Nem o campo escondido do formulário, nem o nome do ficheiro enviado: tudo o que vem do cliente é entrada.
A miniatura vem do YouTube. O vídeo só carrega quando clicas. Abrir no YouTube
XSS: escapar tudo o que se mostra
O XSS (scripts entre sítios) injeta JavaScript na tua página através de dados que mostras sem escapar2. Um comentário com o texto <script>alert(1)</script> corre código no navegador de quem o ler, e uma carga a sério rouba a sessão em vez de mostrar um alerta. A defesa no servidor é o htmlspecialchars que já usas em cada echo, e no cliente é preferir textContent a innerHTML com dados do utilizador, como na lista de tarefas.
Experimenta a neutralização: com escape, a carga aparece na página como texto literal <script>... visível e inofensivo; sem escape, o navegador executa-a. É a mesma cadeia de caracteres nos dois casos, e só a saída decide se é texto ou código. Por isso a regra se aplica na saída, em todas as páginas, mesmo nas “só de leitura”.
CSRF: confirmar quem mandou o pedido
O CSRF (falsificação de pedido entre sítios) aproveita a sessão aberta: uma página maliciosa faz o navegador da vítima submeter um formulário ao teu sítio, e o servidor obedece porque o cookie da sessão vai junto. Segue as setas: a vítima cai na isca, o navegador junta o cookie ao pedido forjado e o servidor obedece por reconhecer a sessão.
A defesa é um segredo que o atacante não conhece: um token por sessão que cada formulário inclui e o servidor confirma.
<?php
session_start();
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(16));
}
?>
<form method="post">
<input type="hidden" name="csrf" value="<?= $_SESSION['csrf'] ?>">
<!-- resto do formulário -->
</form>
No processamento, compara e recusa tudo o que não bater certo. Sem piedade nem mensagens simpáticas para o atacante: pedido sem token válido morre aqui.
<?php
session_start();
$token = $_POST['csrf'] ?? '';
if (!hash_equals($_SESSION['csrf'] ?? '', $token)) {
http_response_code(403);
exit('Pedido recusado.');
}
// daqui para a frente, o pedido veio de um formulário teu
?>
O hash_equals compara em tempo constante, sem atalhos que denunciem os primeiros caracteres. A página maliciosa não lê o token, por isso os pedidos forjados falham a comparação. Aplica o token a todas as ações que alteram algo: encomendar, apagar, mudar a palavra passe. Leituras por GET não precisam dele, o que é mais uma razão para GET nunca alterar nada.
O resto da lista curta
- Travessia de caminho: se o utilizador escolhe um ficheiro,
../../etc/passwdnão pode sair da pasta permitida. Valida o nome contra uma lista ou resolve o caminho e confirma que continua dentro da pasta. - Palavras passe: guarda resumos com
password_hashe confirma compassword_verify, como no login. Nunca guardes nem registes palavras passe em claro. - HTTPS: cifra a conversa entre navegador e servidor. Sem ele, palavras passe e sessões viajam legíveis para quem espreitar a rede. Em produção é obrigatório; em desenvolvimento local, assume que tudo o que corre em HTTP é observável.
Para saber mais
- Página da OWASP sobre XSS, com tipos e vetores de ataque: https://owasp.org/www-community/attacks/xss/.
- Cheat sheet da OWASP de prevenção de XSS, por contexto de saída: https://cheatsheetseries.owasp.org/cheatsheets/Cross%5FSite%5FScripting%5FPrevention%5FCheat%5FSheet.html.
Notas de rodapé
-
Manual do PHP sobre prepared statements com PDO: https://www.php.net/manual/en/pdo.prepared-statements.php. Voltar
-
OWASP, página sobre XSS com os tipos de ataque e os vetores mais comuns: https://owasp.org/www-community/attacks/xss/. Voltar