ろぼいんブログ

HTMLに属性値をエスケープする破壊的変更が導入へ

2025年5月20日、 HTML仕様が更新され mutation XSS (mXSS)脆弱性を防ぐために属性内の<および>をエスケープするようになりました。この変更は、2025年5月28日にベータチャンネルに昇格し、2025年6月24日に安定版になるChrome 138に実装されます。

本記事では、HTML属性のエスケープに関する変更がWeb開発者に与える影響や、想定される破壊的変更について説明します。変更のセキュリティ的背景は、 Security Engineeringブログの関連投稿 にて詳しく解説されています。

何が変わったのか

たとえば、data-content 属性に "<u>hello</u>" を持つ <div> 要素があるとします。このとき、div.outerHTML を読み取るとどうなるでしょうか?

従来は、次のようなHTMLが得られました。

try{(()=>{function a(e){if(!e)return;let t=e.getAttribute("tabindex")!==null,n=e.scrollWidth>e.clientWidth;n&&!t?e.setAttribute("tabindex","0"):!n&&t&&e.removeAttribute("tabindex")}var u=window.requestIdleCallback||(e=>setTimeout(e,1)),i=window.cancelIdleCallback||clearTimeout;function l(e){let t=new Set,n,r;return new ResizeObserver(c=>{c.forEach(o=>t.add(o.target)),n&&clearTimeout(n),r&&i(r),n=setTimeout(()=>{r&&i(r),r=u(()=>{t.forEach(o=>e(o)),t.clear()})},250)})}function d(e,t){e.querySelectorAll?.(".expressive-code pre > code").forEach(n=>{let r=n.parentElement;r&&t.observe(r)})}var s=l(a);d(document,s);var b=new MutationObserver(e=>e.forEach(t=>t.addedNodes.forEach(n=>{d(n,s)})));b.observe(document.body,{childList:!0,subtree:!0});document.addEventListener("astro:page-load",()=>{d(document,s)});})();}catch(e){console.error("[EC] tabindex-js-module failed:",e)}try{(()=>{function i(o){let e=document.createElement("pre");Object.assign(e.style,{opacity:"0",pointerEvents:"none",position:"absolute",overflow:"hidden",left:"0",top:"0",width:"20px",height:"20px",webkitUserSelect:"auto",userSelect:"all"}),e.ariaHidden="true",e.textContent=o,document.body.appendChild(e);let a=document.createRange();a.selectNode(e);let n=getSelection();if(!n)return!1;n.removeAllRanges(),n.addRange(a);let r=!1;try{r=document.execCommand("copy")}finally{n.removeAllRanges(),document.body.removeChild(e)}return r}async function l(o){let e=o.currentTarget,a=e.dataset,n=!1,r=a.code.replace(/\u007f/g,` `);try{await navigator.clipboard.writeText(r),n=!0}catch{n=i(r)}if(!n||e.parentNode?.querySelector(".feedback"))return;let t=document.createElement("div");t.classList.add("feedback"),t.append(a.copied),e.before(t),t.offsetWidth,requestAnimationFrame(()=>t?.classList.add("show"));let c=()=>!t||t.classList.remove("show"),d=()=>{!t||parseFloat(getComputedStyle(t).opacity)>0||(t.remove(),t=void 0)};setTimeout(c,1500),setTimeout(d,2500),e.addEventListener("blur",c),t.addEventListener("transitioncancel",d),t.addEventListener("transitionend",d)}function s(o){o.querySelectorAll?.(".expressive-code .copy button").forEach(e=>e.addEventListener("click",l))}s(document);var u=new MutationObserver(o=>o.forEach(e=>e.addedNodes.forEach(a=>{s(a)})));u.observe(document.body,{childList:!0,subtree:!0});document.addEventListener("astro:page-load",()=>{s(document)});})();}catch(e){console.error("[EC] copy-js-module failed:",e)}
<div data-content="<u>hello</u>"></div>

変更後は、次のようになります。

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

以前は属性内の <> はエスケープされませんでした。今後、これらの文字列は常にエスケープされます。

変わらないこと

今回の変更は、HTMLフラグメントが文字列としてシリアライズされるときの挙動にのみ影響します。影響を受けるのは、innerHTMLouterHTMLにアクセスするケースや、要素に対して getHTML() メソッドを呼び出すようなケースのみです。これらの操作は、既存のDOM構造を受け取り、HTML文字列を生成します。

この変更はHTMLのパース処理には影響しません。次のようなHTMLを考えてみましょう。

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

どちらのdivも同じようにパースされ、div.dataset.content の結果は両方とも "<u>hello</u>" になります。

何が壊れないのか?

getAttributegetAttributeNSdatasetattributes など、DOM APIを用いて属性値を取得する場合、以前と同じデコードされた値、とくに<> がデコードされた値が返されます。

以下の例では、すべての console.log"<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);

何が壊れる可能性があるか?

innerHTMLやouterHTMLで属性値を取得している場合

属性値の抽出に innerHTMLouterHTML を使用している場合、コードが壊れる可能性があります。たとえば次のような少し込み入った例を考えます。

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

このコードは、変更後は異なる挙動になります。以前は content"<u>" でしたが、今後は "&lt;u&gt;" になります。

なお、 正規表現でHTMLをパースするのは推奨されない ことに注意してください。属性値を取得するには、前述のDOM APIを利用してください。

エンドツーエンドテスト

ChromiumでHTMLを生成するCI/CDパイプラインで、期待される静的なHTMLと比較するようなテストを実行している場合、属性値に <> を含んでいるとテストが失敗する可能性があります。

これは想定された破壊的変更であり、<> をそれぞれ &lt;&gt; にエスケープした新しい期待値に更新する必要があります。

まとめ

本記事では、HTML仕様の変更により、属性内の <> がエスケープされるようになり、一部のmutation XSSを防ぐことでセキュリティが向上することを紹介しました。

この変更は、2025年6月24日のChromium(バージョン138)とFirefox(バージョン140)ですべてのユーザーに適用されます。また、2025年9月ごろにリリース予定のSafari 26 Betaにも含まれる予定です。

もしこの変更によってウェブサイトが壊れてしまい、簡単に修正できない場合は、 https://issues.chromium.org/ にバグを報告してください。

追加情報


HTML spec change: escaping < and > in attributes  |  Blog  |  Chrome for Developer ” by Google, licensed under Creative Commons Attribution 4.0 License / translated into Japanese by ろぼいん

おすすめアイテム

※このリンクを経由して商品を購入すると、当サイトの運営者が報酬を得ることがあります。詳細はこちら

このサイトを支援する

Buy Me a CoffeeまたはGitHub Sponsorsで支援していただけると、サイトの運営やコンテンツ制作の励みになります。定期的な支援と一度限りの支援がありますので、お間違いのないようにお願いします。

Buy me a coffee

著者のアイコン画像

生まれた時から、母国語よりも先にJavaScriptを使っていました。ネットの海のどこにもいなくてどこにでもいます。

Webフロントエンドプログラマーで、テクノロジーに関する話題を追いかけています。動画編集やプログラミングが趣味で、たまにデザインなどもやっています。主にTypeScriptを使用したWebフロントエンド開発を専門とし、便利で実用的なブラウザー拡張機能を作成しています。また、個人ブログを通じて、IT関連のニュースやハウツー、技術的なプログラミング情報を発信しています。