htmlspecialchars($text) converte &, <, >, " e ' em &, <, >, " e '. Chame-o em cada pedaço de entrada do usuário que você imprime numa página, e a entrada aparece como texto em vez de ser lida como HTML.
O bloco imprime o mesmo comentário duas vezes, uma cru e uma escapado. Rode e compare as duas linhas na aba Page, depois digite o seu próprio HTML no formulário, por exemplo <h1>big</h1> ou <img src=x>, e clique em Show.
Na linha crua o navegador obedece às tags: a palavra em negrito fica em negrito, e um visitante que digita <script> faz o script dele rodar no navegador de todos os outros leitores. Esse ataque se chama cross-site scripting (XSS). Na linha escapada, os mesmos caracteres chegam como <b> e o navegador os desenha como texto. Mude para a aba Output para ver as entidades que o PHP de fato imprimiu.
O que o htmlspecialchars converte
Cinco caracteres, nada mais. Letras, acentos e emoji passam sem mudança.
O & está na lista porque começa toda entidade: se ele ficasse como está, um comentário que menciona < seria exibido como <.
Escapar atributos, não só texto
Uma entrada do usuário dentro de um atributo precisa de escape tanto quanto. Sem ele, uma aspa no valor fecha o atributo e o resto da entrada vira novos atributos. Aqui o "nome" contrabandeia um atributo style; rode e olhe as duas caixas.
Na caixa insegura, o navegador vê value="Ada" seguido de um novo atributo style, então a caixa fica vermelha e mostra só Ada. Um atacante escreveria onfocus="..." ali em vez de style, e o código dele rodaria. Na caixa segura, cada " virou ", então a string inteira fica dentro do value e aparece como foi digitada.
Desde o PHP 8.1, as flags padrão são ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, então aspas simples também são escapadas e atributos escritos com '...' ficam seguros. Código antigo muitas vezes passa ENT_QUOTES à mão, e no PHP 7 e anteriores isso era obrigatório:
Uma função auxiliar curta para templates
Escrever htmlspecialchars($x, ENT_QUOTES, 'UTF-8') dezenas de vezes num template polui o código, então a maioria dos projetos o envolve numa função de uma letra. Motores de template como Twig e Blade fazem a mesma coisa automaticamente para cada {{ $var }}.
O tipo ?string e o ?? '' importam: passar null para o htmlspecialchars() está depreciado desde o PHP 8.1, e o seu PHP imprimiria um aviso de depreciação para cada usuário sem bio.
Codificação dupla e htmlspecialchars_decode
Se um valor for escapado duas vezes, o leitor vê as entidades: & vira & na primeira vez e &amp; na segunda, que o navegador mostra como &. Normalmente isso significa que o valor foi escapado quando foi salvo e de novo quando foi impresso. Passe double_encode: false para deixar as entidades existentes como estão, e use htmlspecialchars_decode() para voltar.
A correção de verdade é guardar o texto cru e escapar só na saída. double_encode: false é para textos que já contêm entidades que você não criou, como um feed importado.
htmlspecialchars, htmlentities ou strip_tags
Essas três costumam ser confundidas. O bloco roda todas na mesma entrada e mostra, para cada uma, o que o PHP imprime e o que o navegador faz com isso:
- O
htmlspecialchars()escapa os cinco caracteres do HTML. Use para qualquer texto que você imprime no HTML. - O
htmlentities()também transformaéemé. Era útil quando as páginas não eram UTF-8; hoje só deixa o código-fonte mais difícil de ler. - O
strip_tags()apaga as tags e mantém o texto delas. Serve para transformar HTML em texto puro (uma prévia de e-mail, uma meta description), não para segurança: a última linha mostra que um<b>permitido mantém oonclick, e texto colocado dentro de um atributo não é tocado.
Onde o htmlspecialchars não basta
O htmlspecialchars() é o escape certo para texto HTML e atributos entre aspas. Outros lugares de uma página têm outras regras:
- Numa URL,
http_build_query()ouurlencode()codifica o valor; ohtmlspecialchars()então torna válido no HTML o&entre os parâmetros. - No JavaScript, o
json_encode()produz um valor JS válido, e oJSON_HEX_TAGtransforma<e>em\u003Ce\u003E, para que um</script>nos dados não consiga fechar a tag script. - Nunca imprima entrada do usuário num
hrefsem verificar o esquema:htmlspecialchars('javascript:alert(1)')não muda nada e continua rodando quando clicado. Aceite só URLshttpehttps, como mostra a página sobre filter_var.
Para o processamento de formulários que junta tudo isso, veja formulários em PHP.
Perguntas frequentes
O que faz o htmlspecialchars no PHP?
Ele substitui &, <, >, " e ' por &, <, >, " e '. O navegador então exibe esses caracteres em vez de lê-los como HTML, então um <script> digitado num formulário aparece como texto e nunca roda.
Qual a diferença entre htmlspecialchars e htmlentities?
O htmlspecialchars() converte só os cinco caracteres especiais do HTML. O htmlentities() também converte todo caractere que tem uma entidade nomeada, então café vira café. Em páginas UTF-8 os dois são igualmente seguros, e o htmlspecialchars() mantém a saída legível, então é a escolha comum.
Ainda preciso de ENT_QUOTES no PHP 8?
Não por segurança: desde o PHP 8.1 as flags padrão são ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, então aspas simples também são escapadas. Muitos projetos ainda passam ENT_QUOTES, 'UTF-8' explicitamente para que a chamada se comporte igual em versões antigas e fique óbvia para quem lê.
Devo usar htmlspecialchars na entrada ou na saída?
Na saída. Guarde e valide o valor cru, e escape no momento em que imprimi-lo no HTML. Escapar na entrada guarda < no seu banco de dados, atrapalha tamanhos e buscas e leva a escapes duplos como &lt;.
O strip_tags basta para evitar XSS?
Não. O strip_tags() remove tags, mas o parâmetro de tags permitidas mantém os atributos delas, então <b onclick="..."> sobrevive, e ele não faz nada com texto colocado dentro de um atributo. Use htmlspecialchars() ao imprimir entradas do usuário.