Segurança web
Injeção SQL, XSS, CSRF, autorização, travessia de caminhos, HTTPS, palavras-passe e sessões.
O título de um livro pode passar pelo pedido HTTP, pela base de dados e pelo HTML apresentado. Em cada passagem, o programa decide se o interpreta como dado ou como instrução. Preparar SQL impede que o título mude a consulta; escapar a saída impede que se torne marcação ativa. Validar formato e limites responde a outra pergunta, se o título é permitido pela aplicação.
Injeção SQL
Este excerto concatena dados numa instrução SQL:
$sql = "SELECT id FROM utilizador WHERE nome = '$nome'";
Se o atacante escolhe $nome = "' OR 1=1 -- ", a consulta resultante fica:
SELECT id FROM utilizador WHERE nome = '' OR 1=1 -- '
OR 1=1 é sempre verdadeiro e -- comenta o resto. A intenção de procurar um nome tornou-se uma condição que aceita linhas diferentes. O ataque não precisa de executar vários comandos para causar dano.
$stmt = $db->prepare('SELECT id FROM utilizador WHERE nome = ?');
$stmt->execute([$nome]);
O marcador recebe o valor como dado. A string de ataque passa a ser um nome literal e não altera a sintaxe. Mantém os valores fora do texto SQL. Para identificadores, como a coluna de ordenação, escolhe entre nomes fixos autorizados; marcadores não representam identificadores SQL.
No vídeo, identifica a concatenação que permite à entrada mudar a sintaxe SQL. Compara-a com a consulta preparada acima.
XSS e contexto de saída
XSS ocorre quando dados controlados pelo atacante passam a código ou marcação ativa na origem da aplicação. Pode ser persistente, como um comentário guardado; refletido, como uma pesquisa devolvida na resposta; ou baseado no DOM, quando JavaScript coloca entrada num destino perigoso.
// Excerto vulnerável num corpo HTML
echo '<p>' . $comentario . '</p>';
// Correção para apresentar texto nesse contexto
echo '<p>' . htmlspecialchars($comentario, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>';
Com a correção, <script>...</script> aparece como texto. O escape para texto HTML também serve para valores de atributos entre aspas quando aplicado corretamente, mas não valida a semântica de um URL nem torna qualquer contexto seguro. Um href ainda precisa de limitar esquemas aceites; JavaScript embebido e CSS têm regras próprias. Evita construir código com dados do utilizador.
No DOM, prefere textContent para texto e criação de elementos com atributos validados. innerHTML interpreta marcação; não o uses para um comentário cru. Sanitizar HTML permitido exige uma biblioteca adequada, com regras explícitas, não uma regex para apagar <script>.
HttpOnly pode impedir que um script leia o cookie, mas XSS ainda pode fazer pedidos autenticados e ler dados na página. Uma política CSP pode limitar código executável e recursos como defesa adicional; não corrige um destino inseguro de saída.
CSRF
Um pedido de alteração pode chegar de outra página e trazer credenciais enviadas automaticamente pelo navegador. Se o servidor verifica apenas o cookie de sessão, pode executar a ação sem saber se o utilizador quis iniciá-la. POST por si não impede isso.
Um token CSRF é um valor imprevisível associado à sessão. O servidor envia-o no formulário e exige-o no pedido de alteração. A página de outra origem não deve conseguir lê-lo pela política de mesma origem. O token não substitui a autenticação nem a autorização.
Segue o ataque antes do código. A vítima tem sessão válida e visita uma página externa que contém um formulário a apontar para a ação de reserva. Se a política do cookie permitir o envio nesse contexto, o pedido leva a sessão da vítima. Sem verificar a origem ou um token associado ao formulário, o servidor não consegue confirmar que a ação veio do percurso legítimo.
Estes excertos pertencem, respetivamente, à página que mostra o formulário e à ação que o processa:
<?php
session_start();
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
?>
<form action="/actions/reservar.php" method="post">
<input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf'], ENT_QUOTES, 'UTF-8') ?>">
<button>Confirmar reserva</button>
</form>
<?php
session_start();
$esperado = $_SESSION['csrf'] ?? null;
$recebido = $_POST['csrf'] ?? null;
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|| !is_string($esperado) || $esperado === ''
|| !is_string($recebido)
|| !hash_equals($esperado, $recebido)) {
http_response_code(403);
exit('Pedido recusado.');
}
// Seguem-se autenticação, autorização e validação dos dados da reserva.
O teste exige que exista um token esperado não vazio. Comparar dois valores ausentes convertidos em '' aceitaria indevidamente um pedido sem token. hash_equals compara os valores sem a comparação habitual que pode variar com prefixos.
SameSite e verificação de Origin podem reforçar a proteção conforme o desenho. CORS trata leitura entre origens; não prova intenção do utilizador. Um ataque XSS na aplicação pode obter tokens e contornar esta defesa. Não coloques ações de alteração em GET.
Autenticação e autorização
Um utilizador autenticado pode não ter permissão para editar a reserva de outra pessoa. O identificador recebido num campo hidden, URL ou JSON continua a ser entrada. Verifica a associação na operação real:
$stmt = $db->prepare('UPDATE reserva SET quantidade = ? WHERE id = ? AND utilizador_id = ?');
$stmt->execute([$quantidade, $reservaId, $_SESSION['utilizador_id']]);
Compara dois utilizadores. O utilizador A edita a reserva 7, que lhe pertence. O utilizador B tenta editar a reserva 7 alterando o identificador no pedido. O utilizador vem da sessão verificada, não de $_POST['utilizador_id']. A consulta restringe o registo ao dono. Confirma o efeito esperado e devolve um erro quando não existe uma reserva autorizada. Esconder um botão ou impedir uma ação em JavaScript melhora a interface, mas o atacante pode enviar o pedido diretamente.
Palavras-passe e sessão
Guarda um resumo próprio para palavras-passe com password_hash; verifica com password_verify. PASSWORD_DEFAULT escolhe o algoritmo da versão e pode mudar, por isso a coluna deve suportar resumos com tamanho suficiente. O resumo guarda os parâmetros necessários à verificação, incluindo salt.
Salt é público e fica guardado com o resumo; não precisa de ser lembrado pelo utilizador nem enviado como segredo separado. password_hash cria-o automaticamente. Depois de um login válido, password_needs_rehash permite decidir se o resumo deve ser atualizado para os parâmetros atuais.
No registo de Ana e Rui, que escolheram a mesma palavra-passe, cada chamada a password_hash cria um salt aleatório. Os resumos normalmente diferem e conservam os parâmetros de verificação. No login de Ana, password_verify usa os parâmetros do resumo de Ana.
O salt dificulta tabelas pré-calculadas e impede reconhecer palavras-passe iguais apenas comparando os resumos. Não torna uma palavra-passe fraca resistente a tentativas: continua a precisar de limites de tentativas e de mensagens que não revelem contas desnecessariamente. SHA-1 e SHA-256 simples são rápidos e inadequados para guardar palavras-passe, mesmo com salt manual.
Regenera o identificador de sessão ao autenticar ou mudar privilégios. Em produção, usa HTTPS e cookies Secure, HttpOnly e SameSite adequado. Nunca confies num identificador de sessão recebido para autorizar sem recuperar o estado e verificar a ação.
Caminhos e ficheiros
Se readfile($_GET['ficheiro']) recebe ../../config.php, o programa pode revelar um ficheiro fora do conjunto pretendido. Remover literalmente ../ não cobre todas as formas de manipular caminhos.
Quando só há poucos documentos, mapeia identificadores para caminhos fixos:
$ficheiros = ['guia' => __DIR__ . '/../privado/guia.pdf'];
$id = $_GET['id'] ?? null;
if (!is_string($id) || !isset($ficheiros[$id])) {
http_response_code(404);
exit;
}
readfile($ficheiros[$id]);
Esse é um excerto de um endpoint que ainda deve verificar a permissão de leitura. Para uploads, valida tamanho e formato real, escolhe nomes no servidor e guarda fora de pastas onde possam executar. A extensão e o tipo declarados pelo cliente não são prova suficiente.
HTTPS e revisão de uma ação
HTTPS usa TLS para proteger confidencialidade e integridade do tráfego e autenticar o servidor conforme os certificados. O navegador verifica que o certificado corresponde ao nome pedido e encadeia até uma autoridade em que confia, além da validade temporal. Durante a ligação, as partes acordam chaves; a cifra simétrica protege depois os dados. A chave privada do servidor não é enviada ao cliente. Não corrige SQL injection, XSS nem autorização. Um sítio malicioso também pode ter HTTPS.
Ao rever uma ação de reserva, segue o percurso: método correto, sessão válida, token CSRF, dono autorizado, parâmetros com tipo e limites, SQL preparado, alteração consistente e saída no contexto certo. Para cada teste, explica um pedido que falharia sem ele.
Exercícios
Escolher a defesa certa
Uma ação concatena nome numa consulta SQL e mais tarde coloca esse nome em innerHTML. Um colega aplica htmlspecialchars antes de guardar e afirma que resolveu tudo. Identifica os problemas e indica as duas defesas necessárias.
Pista
São duas leituras distintas: SQL e marcação HTML.
Mais uma pista
Escapar HTML não separa dados de SQL.
Solução
A consulta continua vulnerável a injeção SQL e precisa de valores em marcadores preparados. A saída deve tratar o nome como texto, usando textContent no cliente ou escape correto no contexto HTML do servidor. Guardar texto já escapado também pode causar dupla codificação e não serve todos os contextos. Validação de formato pode complementar, mas não substitui nenhuma destas defesas.
Confere a tua resposta:
- Separei SQL de saída HTML.
- Escolhi prepare e destino de texto/escape de saída.
Erros frequentes
Não escolher uma função universal de escape nem tratar dados da base como automaticamente seguros.
Encontrar um CSRF sem token
O servidor compara hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? ''). Que acontece quando ambos faltam? Corrige a condição e explica por que POST e CORS não chegam.
Pista
Dois valores vazios são iguais.
Mais uma pista
Exige um token esperado existente, não vazio e string, e um recebido string.
Solução
A condição ingénua compara vazio com vazio e aceita. Quando ambos faltam, hash_equals("", "") é verdadeiro. A ação deve verificar método POST, existência e tipo do token da sessão, valor esperado não vazio, tipo do token recebido e só depois hash_equals. Um formulário de outra origem pode enviar POST com credenciais automaticamente aplicáveis. CORS restringe leitura da resposta, não prova intenção nem impede todos os envios.
Confere a tua resposta:
- Mostrei a aceitação de vazio/vazio.
- Exigi token válido antes da comparação.
Erros frequentes
Não gerar um token só na ação e assumir que isso comprova o formulário; o valor deve estar ligado ao fluxo legítimo.
Autorizar o registo correto
Uma ação permite editar reserva_id vindo do cliente se existir utilizador_id na sessão. O utilizador altera reserva_id para o de outra pessoa. Qual a verificação em falta? Mostra uma condição SQL apropriada.
Pista
Ter sessão não concede acesso a todos os registos.
Solução
Falta autorizar a propriedade do registo. Uma condição apropriada é WHERE id = :reserva AND utilizador_id = :utilizador, com reserva recebida e validada e utilizador obtido da sessão. Ambos são valores preparados. Confirma que houve um registo autorizado a alterar, e verifica também os novos dados e CSRF. Esconder o botão não protege o endpoint.
Confere a tua resposta:
- Usei o dono vindo da sessão.
- Restringi o próprio UPDATE ao registo autorizado.
Erros frequentes
Não aceitar utilizador_id de um hidden como autoridade para a permissão.
Explicar salt e HTTPS
Dois utilizadores escolhem a mesma palavra-passe. O sistema usa password_hash para cada um. Os resumos têm de ser iguais? Um salt salva a palavra-passe 123456? HTTPS resolveria uma consulta SQL concatenada?
Pista
O resumo inclui salt e parâmetros; o algoritmo é próprio para palavras-passe.
Mais uma pista
Distingue ataque offline à palavra-passe e proteção do transporte.
Solução
Os resumos não têm de ser iguais e normalmente diferem por salts distintos. Isso dificulta pré-cálculo e comparação direta, mas 123456 continua fácil de tentar. password_verify verifica a palavra-passe contra cada resumo. HTTPS protege o transporte, não a interpretação SQL no servidor, que continua a exigir preparação. Regenera a sessão depois de autenticar.
Confere a tua resposta:
- Expliquei resumos diferentes e limite do salt.
- Distingui HTTPS de defesa SQL.
Erros frequentes
Não usar SHA-1 simples com salt nem inferir segurança da aplicação a partir do cadeado HTTPS.
Proteger o ficheiro e o seu acesso
Uma ação autenticada recebe id=guia e mapeia-o para um PDF fixo fora da pasta pública. O programador põe uma cópia em public/guia.pdf para facilitar downloads. Quem não tem sessão consegue descarregar essa cópia por URL direto? Explica os dois controlos necessários.
Pista
O servidor pode servir um ficheiro público sem executar a ação PHP.
Solução
Sim, se o servidor servir a pasta public sem controlo adicional, o URL direto contorna a ação. O mapa fixo impede escolher caminhos arbitrários, mas a ação continua a precisar de verificar sessão e permissão para aquele documento. Guarda o PDF fora da raiz pública e disponibiliza-o só pelo endpoint autorizado. Não confies num nome difícil de adivinhar como controlo de acesso.
Confere a tua resposta:
- Separei a seleção de caminhos da autorização.
- Retirei a cópia pública e mantive o controlo no endpoint.
Erros frequentes
Não confundir caminho autorizado com utilizador autorizado nem considerar um ficheiro privado só por não ter uma ligação visível.
