O Cross-Site Scripting (XSS) é uma das vulnerabilidades mais comuns e perigosas em aplicações web. Ele ocorre quando um atacante consegue injetar código malicioso (geralmente JavaScript) em páginas visualizadas por outros usuários. Em aplicações PHP, a prevenção de XSS é uma responsabilidade crucial do desenvolvedor, pois os dados fornecidos pelos usuários são frequentemente exibidos em páginas HTML. Nesta aula, vamos explorar as melhores práticas para evitar essa vulnerabilidade, desde o escape básico com htmlspecialchars até a implementação de políticas de segurança de conteúdo (CSP).

Dominar a prevenção de XSS é essencial para qualquer desenvolvedor PHP que queira criar aplicações seguras e confiáveis. Você aprenderá não apenas a função htmlspecialchars, mas também quando e como aplicá-la em diferentes contextos, além de entender como o CSP pode adicionar uma camada extra de defesa. Vamos começar com uma visão geral do problema.

O que é

XSS (Cross-Site Scripting) é um tipo de ataque de injeção em que scripts maliciosos são injetados em conteúdo confiável de sites. O atacante aproveita vulnerabilidades na aplicação para inserir código JavaScript que será executado no navegador de outros usuários. Isso pode permitir roubo de cookies, sessões, redirecionamentos para sites maliciosos, alteração de conteúdo da página e outras ações prejudiciais.

Existem três tipos principais de XSS: Refletido, Armazenado e Baseado em DOM. No refletido, o payload malicioso é incluído na URL ou em parâmetros de requisição e refletido imediatamente na resposta. No armazenado, o payload é persistido no servidor (por exemplo, em um banco de dados) e exibido posteriormente a outros usuários. O baseado em DOM ocorre quando o JavaScript no navegador manipula o DOM de forma insegura, sem que o servidor esteja diretamente envolvido. Em PHP, o foco principal é evitar os dois primeiros tipos, garantindo que qualquer dado não confiável seja escapado antes de ser exibido.

Um exemplo clássico de vulnerabilidade XSS em PHP é quando um formulário aceita um nome de usuário e o exibe sem tratamento:

<?php
$nome = $_POST['nome'];
echo "Olá, $nome!";
?>

Se o usuário digitar <script>alert('hacked')</script>, o navegador executará esse script. Para prevenir, é fundamental escapar qualquer saída que contenha dados do usuário.

htmlspecialchars

A função htmlspecialchars é a ferramenta mais básica e essencial para prevenir XSS em PHP. Ela converte caracteres especiais em entidades HTML, impedindo que o navegador os interprete como código. A função tem a seguinte assinatura:

htmlspecialchars(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE, ?string $encoding = null, bool $double_encode = true): string

Os caracteres que são convertidos são: & (ampersand), < (menor que), > (maior que), " (aspas duplas) e, se ENT_QUOTES for definido, também ' (aspas simples). Isso garante que, ao exibir dados, eles sejam tratados como texto simples e não como código HTML.

O uso mais comum é:

echo htmlspecialchars($dados, ENT_QUOTES, 'UTF-8');

É importante definir o encoding correto (geralmente UTF-8) e usar ENT_QUOTES para escapar tanto aspas duplas quanto simples, pois ambos podem ser usados em atributos HTML. Um exemplo prático:

$comentario = $_POST['comentario'];
?>
<p><?php echo htmlspecialchars($comentario, ENT_QUOTES, 'UTF-8'); ?></p>

Se o usuário enviar <script>alert('XSS')</script>, o resultado será exibido como texto literal, sem executar. No entanto, htmlspecialchars não é uma solução universal: ela só é eficaz no contexto de HTML (entre tags e em atributos entre aspas). Em outros contextos, como dentro de JavaScript ou CSS, é necessário um escape diferente.

Escape no contexto certo

A prevenção de XSS não se resume a usar htmlspecialchars em todos os lugares. O escape deve ser adequado ao contexto em que os dados são inseridos. Existem quatro contextos principais em uma página web: HTML (corpo), atributos HTML, JavaScript e CSS. Cada um exige uma estratégia de escape específica.

1. Contexto HTML (corpo): Quando os dados são inseridos entre tags, como <p>...</p>, o escape deve converter <, > e & em entidades. htmlspecialchars é suficiente.

2. Contexto de atributo HTML: Se os dados são inseridos em um atributo, como value="...", é necessário escapar também aspas duplas e simples, para que o atributo não seja quebrado. Use ENT_QUOTES.

3. Contexto JavaScript: Se os dados são passados para JavaScript, por exemplo, em uma variável ou dentro de uma string, htmlspecialchars não é suficiente. Você deve usar json_encode para gerar uma string JSON segura, que escapará caracteres como aspas e barras. Por exemplo:

$dados = ['nome' => $nome_usuario];
?>
<script>
    var dados = <?php echo json_encode($dados, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;
</script>

As flags JSON_HEX_* garantem que caracteres como <, >, aspas e ampersand sejam convertidos em sequências de escape Unicode, evitando que o navegador interprete como HTML.

4. Contexto CSS: Se os dados são inseridos em estilos, como background: url(...), o escape é mais complexo. A melhor prática é evitar ao máximo inserir dados do usuário em CSS. Se for necessário, use funções como addcslashes ou validação rigorosa, mas o ideal é não permitir.

Uma abordagem moderna é usar frameworks ou bibliotecas de template que lidam com escape automático, como Twig ou Blade, que aplicam o escape correto por contexto. Em PHP puro, você deve estar atento a cada situação.

CSP (visão geral)

Content Security Policy (CSP) é uma camada adicional de segurança que pode ser implementada via cabeçalho HTTP ou meta tag. Ela permite que você controle quais recursos o navegador pode carregar, como scripts, estilos, imagens, etc. Ao restringir a execução de scripts a fontes confiáveis, o CSP pode mitigar significativamente os impactos de XSS, mesmo que um payload consiga ser injetado.

No PHP, você pode enviar o cabeçalho CSP usando header():

header("Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.example.com; style-src 'self' 'unsafe-inline'");

Isso define que apenas scripts do mesmo domínio e de https://apis.example.com podem ser executados, e estilos apenas do mesmo domínio ou inline (com ressalvas). É importante testar a política para não quebrar funcionalidades legítimas. Uma política muito restritiva pode impedir o uso de bibliotecas externas ou scripts inline.

CSP é uma defesa em profundidade: mesmo que o escape falhe, a política pode bloquear a execução do script malicioso. No entanto, não substitui o escape correto; é uma camada complementar. Considere também usar nonces ou hashes para permitir scripts inline específicos, se necessário.

Boas práticas e observações finais

Além de escapar saídas e usar CSP, outras práticas ajudam a prevenir XSS: validar e sanitizar entradas (embora a validação não seja suficiente sozinha), usar prepared statements para evitar SQL Injection (que pode levar a XSS armazenado), e manter o PHP e bibliotecas atualizados. Sempre trate dados do usuário como não confiáveis e aplique o escape no ponto de saída. Considere usar um template engine com escape automático para reduzir erros humanos.

Lembre-se: a segurança é um processo contínuo. Teste sua aplicação com ferramentas de análise de segurança e revise o código regularmente. A prevenção de XSS é uma habilidade fundamental que todo desenvolvedor PHP deve dominar.

Exercícios

  1. Explique por que o seguinte código é vulnerável a XSS e como corrigi-lo:

    echo "Olá, " . $_GET['nome'] . "!";
    

    ✓ Resposta: O código exibe diretamente o parâmetro nome sem escapar, permitindo injeção de HTML/JavaScript. Corrigir usando htmlspecialchars:
    echo "Olá, " . htmlspecialchars($_GET['nome'], ENT_QUOTES, 'UTF-8') . "!";
  2. Qual a diferença entre htmlspecialchars e htmlentities? Quando usar cada um?

    ✓ Resposta: htmlspecialchars converte apenas os caracteres essenciais para prevenir XSS: &, <, >, " e ' (com ENT_QUOTES). htmlentities converte todos os caracteres que têm entidades HTML, incluindo acentos e símbolos. Use htmlspecialchars para segurança e performance; htmlentities pode ser usado quando se quer exibir todos os caracteres como entidades, mas é mais lento e pode causar problemas de encoding.
  3. Como você escaparia dados para inserção dentro de um atributo HTML, como value? Escreva um exemplo.

    ✓ Resposta: Usar htmlspecialchars($dados, ENT_QUOTES, 'UTF-8') para escapar aspas duplas e simples. Exemplo:
    <input type="text" name="nome" value="<?php echo htmlspecialchars($nome, ENT_QUOTES, 'UTF-8'); ?>">
  4. Explique o que é Content Security Policy e como ela ajuda a mitigar XSS.

    ✓ Resposta: CSP é um cabeçalho HTTP que define políticas de segurança para o navegador, restringindo quais fontes de scripts, estilos, imagens etc. são permitidas. Se um atacante injetar um script, o navegador bloqueará sua execução se a origem não estiver na política. Isso reduz o impacto do XSS, mas não elimina a necessidade de escape.
  5. Escreva um exemplo de como enviar um cabeçalho CSP em PHP que permita scripts apenas do próprio domínio e de um CDN confiável.

    ✓ Resposta:
    header("Content-Security-Policy: script-src 'self' https://cdn.trusted.com; default-src 'self'");

Referências