Segurança da aplicação web
SQL injection, XSS, CSRF, autorização e palavras-passe, com exemplos de defesa por contexto.
O email recebido no login é texto controlado pelo cliente. Se a aplicação o concatenar numa consulta, esse texto pode passar a ser sintaxe SQL. A defesa depende de onde a entrada é interpretada. Nos pedidos sobre bilhetes, é preciso verificar também quem pede e se pode operar sobre aquele objeto. A validação no navegador pode ser contornada.
SQL injection: separar código de valores
Construir SQL por concatenação permite que um valor passe a ser sintaxe:
// Vulnerável: a entrada faz parte do texto da instrução.
$sql = "SELECT id FROM utilizadores WHERE email = '" . $email . "'";
Usa parâmetros preparados:
$stmt = $pdo->prepare('SELECT id FROM utilizadores WHERE email = :email');
$stmt->execute(['email' => $email]);
O valor é tratado como dado. Um apóstrofo deixa de fechar uma string do SQL. Parametrizar um valor não parametriza identificadores como nomes de colunas ou palavras-chave. Se a pessoa escolher a ordenação, mapeia a opção para uma lista de colunas autorizadas, sem concatenar o texto recebido.
O ORM ajuda quando usa parâmetros corretamente. Uma expressão raw construída com entrada continua vulnerável. Limita também os privilégios da conta da aplicação: uma leitura não precisa de permissões para alterar o esquema.
XSS: o contexto decide a codificação
Cross-site scripting faz o navegador interpretar dados controlados pelo atacante como código da página. Pode ser refletido na resposta, guardado e mostrado mais tarde, ou criado no cliente ao inserir dados de forma insegura no DOM.
Para texto dentro de HTML, em PHP:
echo htmlspecialchars($comentario, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
No Blade, {{ $comentario }} escapa HTML e {!! $comentario !!} imprime sem essa proteção. No DOM, prefere textContent para texto, porque innerHTML interpreta HTML e só deve receber conteúdo tratado para esse propósito.
A codificação depende do destino. Escapar texto HTML não torna segura uma interpolação dentro de JavaScript, CSS ou um URL. Num URL também tens de verificar o esquema, para não aceitar javascript:. Quando a funcionalidade permite HTML formatado, usa uma biblioteca de sanitização mantida com uma lista restrita de elementos e atributos, em vez de remover apenas <script>.
Uma política CSP pode reduzir certos ataques, mas não substitui codificação por contexto e sanitização. HttpOnly protege a leitura direta do cookie; um script injetado pode ainda agir com a sessão da vítima.
CSRF: a sessão é enviada automaticamente
Em cross-site request forgery, uma página de outra pessoa induz um pedido ao sistema usando credenciais que o navegador envia, como cookies. O atacante não precisa de conhecer o identificador da sessão nem de ler a resposta para causar uma alteração.
Uma compra baseada em sessão deve usar um token imprevisível associado ao contexto autorizado e validado pelo servidor. No Laravel, formulários protegidos pelo middleware adequado incluem @csrf. O token só é útil se for verificado; não deve ser colocado num URL que aparece em históricos e registos.
SameSite e verificação de origem são defesas complementares, não substitutos do token. O valor Lax continua a enviar o cookie em navegações de topo com método seguro: se alguma operação de escrita estiver acessível por GET, essa proteção não a impede, por isso nenhuma alteração de estado pode depender só do método do pedido. Além disso, a comparação é feita ao nível do domínio registável, não da origem completa: dois subdomínios diferentes do mesmo domínio contam como mesmo site. Um método POST sozinho não protege, e CORS não impede todos os envios de formulários. XSS pode contornar muitas defesas CSRF porque executa código na origem legítima.
Autenticado não significa autorizado
O comprador 7 pode estar autenticado e pedir GET /bilhetes/900, pertencente ao comprador 8. A aplicação tem de testar a relação entre a identidade e o recurso. Esconder o link ou usar um identificador difícil de adivinhar não cria essa permissão.
// Excerto Laravel com Illuminate\Support\Facades\Gate importado.
$bilhete = Bilhete::findOrFail($id);
Gate::authorize('view', $bilhete);
Aplica o controlo em todos os caminhos, incluindo API, downloads e operações em lote. Recusa por defeito e concede apenas as permissões necessárias. O papel «comprador» pode dar acesso à funcionalidade, mas a pertença do bilhete decide que objeto pode consultar.
Receber utilizador_id=8 no formulário não autoriza criar uma compra para 8. Usa a identidade autenticada quando a operação é «comprar para mim». $fillable controla preenchimento em massa de atributos, não verifica quem pode operar sobre cada objeto.
Palavras-passe e fixação de sessão
Guarda palavras-passe com um algoritmo de hash próprio para palavras-passe, como Argon2id ou bcrypt, através de uma API mantida. SHA-256 simples é demasiado rápido para este objetivo. Hash não tem uma chave de desencriptação; cifragem permite recuperar o original com uma chave. Uma palavra-passe não precisa de ser recuperada para verificar o login.
Em PHP, password_hash gera o hash com os parâmetros e salt necessários; password_verify verifica-o. O hash guardado já contém a informação que a API precisa. Não compares um novo hash gerado ao texto do anterior, porque os salts podem diferir.
Após autenticar, regenera o identificador da sessão para impedir session fixation. Uma mensagem de login genérica evita revelar qual parte das credenciais existe. Limitação de tentativas, recuperação com tokens de uso único e expiração completam a proteção.
<?php
$texto = '<img src=x onerror="alert(1)">';
echo htmlspecialchars($texto, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "\n";
$hash = password_hash('exemplo_que_nao_e_segredo', PASSWORD_DEFAULT);
echo password_verify('exemplo_que_nao_e_segredo', $hash) ? "certa\n" : "recusada\n";
echo password_verify('outra', $hash) ? "certa\n" : "recusada\n";Executa o código para ver a saída e os gráficos.
O bloco isola duas APIs: codificação de texto para HTML e verificação de palavra-passe. Não implementa login, CSRF ou políticas de acesso, por isso não introduzas palavras-passe pessoais nestes exemplos.
Identificar a defesa numa pergunta
| Situação | Defesa principal |
|---|---|
| Valor torna-se parte da sintaxe SQL. | Consulta parametrizada. |
| Comentário torna-se HTML ativo. | Codificação ou sanitização apropriada ao contexto. |
| Cookie da vítima acompanha uma operação induzida. | Proteção CSRF validada no servidor. |
| Pessoa consulta o bilhete de outra. | Autorização sobre o recurso. |
| Identificador de sessão anterior ao login é reutilizado. | Regeneração da sessão. |
| Base de dados de palavras-passe é exposta. | Hash resistente com parâmetros adequados, além de proteção do armazenamento. |
São mecanismos diferentes: uma consulta parametrizada não impede alguém de consultar um objeto para o qual não tem autorização, e um token CSRF não impede XSS.
Um pedido ao bilhete alheio, camada a camada
O comprador 7 pede GET /bilhetes/900, mas o bilhete pertence ao comprador 8. Encontrar a linha com SQL seguro não concede permissão para a devolver.
Codificar o título impediria HTML ativo, mas não tornaria lícito mostrar os dados ao comprador 7.
A autorização testa a identidade 7 contra o proprietário 8 e recusa a leitura antes de gerar a representação. Para este pedido de leitura, um token CSRF não substitui a decisão sobre o objeto. Numa escrita, um token válido também não concede permissões; pode ser obtido e enviado por uma pessoa autenticada que tenta operar sobre um bilhete alheio.
Exercícios
Atacar sem ler a resposta
Um atacante precisa de ler a resposta da compra para realizar CSRF?
Pista
O objetivo pode ser alterar, não obter informação.
Mais uma pista
Um formulário pode enviar uma operação com cookies.
Solução
Não precisa de ler. Usa proteção CSRF validada e métodos apropriados, além das defesas complementares.
- Não. Basta induzir uma alteração com as credenciais automáticas da vítima.. A operação pode ter efeito mesmo sem expor o corpo ao atacante.
- Sim, tem de ler o cookie.. O navegador pode enviá-lo sem o atacante o conhecer.
- Sim, CORS impede a alteração.. CORS não bloqueia todos os pedidos que causam efeitos.
Erros frequentes
Não confundas bloquear leitura com bloquear efeito.
Verificar sem recuperar a palavra-passe
Por que não se compara um password_hash novo diretamente
com o hash guardado?
Pista
Dois hashes da mesma palavra-passe podem ser diferentes.
Mais uma pista
O formato guarda parâmetros e salt para a verificação.
Solução
A API gera salts próprios; dois textos de hash podem diferir.
password_verify usa o formato guardado para testar a
palavra-passe.
Confere a tua resposta:
- Reconheço salts diferentes.
- Uso password_verify.
- Não preciso de recuperar o original.
Erros frequentes
Não uses SHA-256 rápido nem cifragem recuperável como substituto automático de um hash de palavras-passe.
Uma defesa depende do destino
Escapar texto HTML torna segura qualquer string interpolada num bloco JavaScript?
Pista
Há dois interpretadores com regras diferentes.
Mais uma pista
É preferível evitar a interpolação em código ativo.
Solução
Não. Escolhe uma API e serialização próprias para o destino, ou mantém os dados fora de código ativo. CSP acrescenta defesa, não corrige a interpolação.
- Não. O contexto JavaScript exige tratamento próprio.. Codificação de texto HTML não é uma defesa universal.
- Sim, todas as linguagens interpretam os caracteres da mesma forma.. Os contextos e parsers diferem.
- Sim, se o site tiver HTTPS.. HTTPS protege o transporte, não essa interpretação.
Erros frequentes
Não chames «sanitização» a qualquer função sem indicar contexto.
Distinguir injeção e acesso alheio
O login constrói a consulta abaixo por concatenação. Separadamente, GET /bilhetes/900 carrega e mostra o bilhete 900 a qualquer comprador autenticado, embora pertença ao comprador 8. O atacante é o comprador 7. Explica entrada, efeito e correção de cada falha. HTTPS e um token CSRF válido resolvem alguma delas?
$sql = "SELECT id FROM utilizadores WHERE email = '" . $email . "'";Pista
Identifica qual interpretador ou regra é contornado em cada caminho.
Solução
Com email ' OR '1'='1, a consulta passa a terminar em WHERE email = '' OR '1'='1', uma condição verdadeira para todas as linhas. Usa uma consulta preparada com um parâmetro :email, para o valor nunca se tornar sintaxe.
O pedido de 7 ao bilhete de 8 pode ter SQL perfeitamente parametrizado e continuar a expor dados. Verifica no servidor uma policy sobre a operação view e o bilhete carregado, recusando o acesso de 7.
HTTPS protege o transporte. O token CSRF trata pedidos induzidos com credenciais automáticas. Nenhum substitui parâmetros SQL ou autorização sobre o objeto.
Confere a tua resposta:
- Mostro como a entrada altera a sintaxe SQL.
- Uso parâmetros para valores.
- Recuso bilhete alheio por autorização.