Mudança na especificação HTML: escape de < e > em atributos

Michał Bentkowski
Michał Bentkowski

Publicado em 12 de junho de 2025

Em 20 de maio de 2025, a especificação HTML foi atualizada para escapar de < e > em atributos, ajudando a evitar vulnerabilidades de XSS de mutação (mXSS, na sigla em inglês). Essa mudança foi lançada no Chrome 138, que foi promovido para Beta em 28 de maio de 2025 e vai se tornar estável em 24 de junho de 2025.

Esta postagem detalha o impacto da mudança de escape de atributos HTML nos desenvolvedores da Web e possíveis falhas. O raciocínio de segurança por trás dessa mudança é explicado na nossa postagem relacionada no blog de engenharia de segurança.

O que mudou

Suponha que você tenha um elemento <div> cujo atributo data-content tenha um valor de "<u>hello</u>". O que acontece quando você lê div.outerHTML?

Historicamente, você receberia o seguinte HTML:

<div data-content="<u>hello</u>"></div>

Depois da mudança, você vai receber o seguinte HTML:

<div data-content="&lt;u&gt;hello&lt;/u&gt;"></div>

Antes, nem < nem > eram escapados em atributos. Agora, esses dois caracteres são sempre escapados.

O que não mudou

A mudança modifica exclusivamente como os fragmentos HTML são convertidos de volta em uma representação de string durante a serialização. O impacto é limitado a cenários em que as propriedades innerHTML ou outerHTML são acessadas ou quando o método getHTML() é invocado em um elemento. Essas operações usam a estrutura DOM existente e produzem uma representação HTML textual.

Essa mudança não afeta a análise de HTML. Considere o seguinte HTML:

<div id="div1" data-content="<u>hello</u>"></div>
<div id="div2" data-content="&lt;u&gt;hello&lt;/u&gt;"></div>

Os dois divs serão analisados exatamente da mesma maneira e, em ambos os casos, div.dataset.content vai retornar "<u>hello</u>".

O que não vai quebrar?

Se você usar qualquer API DOM, como getAttribute, getAttributeNS, dataset ou attributes, para recuperar valores de atributos, eles vão retornar os mesmos valores decodificados anteriormente, especificamente com < e > decodificados.

Considere o exemplo a seguir, em que todas as linhas console.log vão registrar "<u>":

<div data-content="&lt;u&gt;"></div>
const div = document.querySelector("div");
// All of the following will log "<u>"
console.log(div.getAttribute("data-content"));
console.log(div.dataset.content);
console.log(div.attributes['data-content'].value);

O que pode quebrar?

innerHTML e outerHTML para receber atributos

Se você usar innerHTML ou outerHTML para extrair o valor de um atributo, seu código poderá ser corrompido. Considere o exemplo a seguir, embora um pouco complicado:

<div data-content="<u>"></div>
const div = document.querySelector("div");
const content = div.outerHTML.match(/"([^"]+)"/)[1];
console.log(content);

Esse código vai apresentar um comportamento diferente após essa mudança. Antes, content era igual a "<u>", mas agora é "&lt;u&gt;".

Não é recomendado analisar HTML com expressões regulares. Se você precisar extrair um valor de um atributo, use as APIs do DOM descritas nas seções anteriores.

Testes de ponta a ponta

Se você tiver um pipeline de CI/CD em que usa o Chromium para gerar HTML e tiver escrito testes para comparar o HTML a um valor esperado estático, esses testes poderão falhar se algum atributo contiver < ou >.

Esse é um erro esperado. Você precisa atualizar o valor esperado para que todos os caracteres < e > sejam convertidos em &lt; e &gt;,, respectivamente.

Resumo

Esta postagem do blog descreveu uma mudança na especificação do HTML que vai fazer com que os navegadores comecem a escapar de < e > em atributos para melhorar a segurança, evitando algumas instâncias de XSS de mutação.

A mudança vai estar disponível para todos os usuários em 24 de junho de 2025 no Chromium (versão 138) e no Firefox (versão 140). Ele também está incluído na versão Beta do Safari 26, que deve ser lançada por volta de setembro de 2025.

Se você acredita que essa mudança quebrou seu site e não tem uma maneira fácil de corrigir o problema, registre um bug em https://issues.chromium.org/.

Informações adicionais