SSTエンジニアブログ

SSTのエンジニアによるWebセキュリティの技術を中心としたエンジニアブログです。

inert属性の指定されたHTML要素でもイベントを発火させたい!

はじめに

XSS

こんにちは、CTOのはせがわです!

PortSwigger社のブログ記事「Exploiting XSS in hidden inputs and meta tags | PortSwigger Research」おもしろいですね。

Chromeなどのブラウザーに新しく実装されたポップアップを表示させるための機能であるポップオーバー APIを用いて、 <input type="hidden"> に popover 属性を指定することでhidden要素でもイベントを発火させるという解説です。hidden 要素ではvalue属性に対して " がそのまま書き込めることによるXSSがよく見つかるものの、イベントハンドラが動作しないため実際の攻撃へとつなげることは難しいことが多かったのですが、popoverを用いることでより攻撃が成功する可能性が高まりそうです。

portswigger.net

inert 属性

さて、「HTML要素がイベントを受け取らない」という点では、hidden だけでなく inert 属性 というものもあります。HTML要素にinert属性を指定することで、その要素(およびその子孫)が不活性化されユーザー操作などのイベントが無視されるようになります。

<div inert onmouseover="console.log('ここは呼び出されない')">
  不活性化されたdiv要素。この要素の上でマウスを動かしても console.log は実行されない。
</div>

子孫を含めて不活性化されるため、複数のコントロールを含むセクション全体の操作を無効にするような場合にはコントロールに対して個別に disabled 属性を指定するよりもコードが簡略化できます。

inert 属性でもイベントを発火させたい!

そしてここからが本番です。イベントを受け取らない要素でXSSがあった場合に、うまく攻撃につなげる方法はあるのでしょうか。

<div inert>
  不活性化されたdiv要素
  <input type="text" value="ここにXSS。" onmouseover="alert(1)">
</div>

このHTMLでは、input 要素で " を挿入できることによるXSSがあるものの div 要素にinert属性が指定されているためにイベントハンドラが動かせないという想定です。 まずは思いつく限り*1のイベントハンドラを列挙して埋め込んでみましょう。以下、Google Chrome 114 で動作を確認しています。

<div inert>
  <input type="text" onabort="console.log('onabort')" onanimationend="console.log('onanimationend')" onanimationiteration="console.log('onanimationiteration')" onanimationstart="console.log('onanimationstart')" onauxclick="console.log('onauxclick')" onbeforecopy="console.log('onbeforecopy')" onbeforecut="console.log('onbeforecut')" onbeforeinput="console.log('onbeforeinput')" onbeforematch="console.log('onbeforematch')" onbeforepaste="console.log('onbeforepaste')" onbeforetoggle="console.log('onbeforetoggle')" onbeforexrselect="console.log('onbeforexrselect')" onblur="console.log('onblur')" oncancel="console.log('oncancel')" oncanplay="console.log('oncanplay')" oncanplaythrough="console.log('oncanplaythrough')" onchange="console.log('onchange')" onclick="console.log('onclick')" onclose="console.log('onclose')" oncontentvisibilityautostatechange="console.log('oncontentvisibilityautostatechange')" oncontextlost="console.log('oncontextlost')" oncontextmenu="console.log('oncontextmenu')" oncontextrestored="console.log('oncontextrestored')" oncopy="console.log('oncopy')" oncuechange="console.log('oncuechange')" oncut="console.log('oncut')" ondblclick="console.log('ondblclick')" ondrag="console.log('ondrag')" ondragend="console.log('ondragend')" ondragenter="console.log('ondragenter')" ondragleave="console.log('ondragleave')" ondragover="console.log('ondragover')" ondragstart="console.log('ondragstart')" ondrop="console.log('ondrop')" ondurationchange="console.log('ondurationchange')" onemptied="console.log('onemptied')" onended="console.log('onended')" onerror="console.log('onerror')" onfocus="console.log('onfocus')" onformdata="console.log('onformdata')" onfullscreenchange="console.log('onfullscreenchange')" onfullscreenerror="console.log('onfullscreenerror')" ongotpointercapture="console.log('ongotpointercapture')" oninput="console.log('oninput')" oninvalid="console.log('oninvalid')" onkeydown="console.log('onkeydown')" onkeypress="console.log('onkeypress')" onkeyup="console.log('onkeyup')" onload="console.log('onload')" onloadeddata="console.log('onloadeddata')" onloadedmetadata="console.log('onloadedmetadata')" onloadstart="console.log('onloadstart')" onlostpointercapture="console.log('onlostpointercapture')" onmousedown="console.log('onmousedown')" onmouseenter="console.log('onmouseenter')" onmouseleave="console.log('onmouseleave')" onmousemove="console.log('onmousemove')" onmouseout="console.log('onmouseout')" onmouseover="console.log('onmouseover')" onmouseup="console.log('onmouseup')" onmousewheel="console.log('onmousewheel')" onpaste="console.log('onpaste')" onpause="console.log('onpause')" onplay="console.log('onplay')" onplaying="console.log('onplaying')" onpointercancel="console.log('onpointercancel')" onpointerdown="console.log('onpointerdown')" onpointerenter="console.log('onpointerenter')" onpointerleave="console.log('onpointerleave')" onpointermove="console.log('onpointermove')" onpointerout="console.log('onpointerout')" onpointerover="console.log('onpointerover')" onpointerrawupdate="console.log('onpointerrawupdate')" onpointerup="console.log('onpointerup')" onprogress="console.log('onprogress')" onratechange="console.log('onratechange')" onreset="console.log('onreset')" onresize="console.log('onresize')" onscroll="console.log('onscroll')" onscrollend="console.log('onscrollend')" onsearch="console.log('onsearch')" onsecuritypolicyviolation="console.log('onsecuritypolicyviolation')" onseeked="console.log('onseeked')" onseeking="console.log('onseeking')" onselect="console.log('onselect')" onselectionchange="console.log('onselectionchange')" onselectstart="console.log('onselectstart')" onslotchange="console.log('onslotchange')" onstalled="console.log('onstalled')" onsubmit="console.log('onsubmit')" onsuspend="console.log('onsuspend')" ontimeupdate="console.log('ontimeupdate')" ontoggle="console.log('ontoggle')" ontransitioncancel="console.log('ontransitioncancel')" ontransitionend="console.log('ontransitionend')" ontransitionrun="console.log('ontransitionrun')" ontransitionstart="console.log('ontransitionstart')" onvolumechange="console.log('onvolumechange')" onwaiting="console.log('onwaiting')" onwebkitanimationend="console.log('onwebkitanimationend')" onwebkitanimationiteration="console.log('onwebkitanimationiteration')" onwebkitanimationstart="console.log('onwebkitanimationstart')" onwebkitfullscreenchange="console.log('onwebkitfullscreenchange')" onwebkitfullscreenerror="console.log('onwebkitfullscreenerror')" onwebkittransitionend="console.log('onwebkittransitionend')" onwheel="console.log('onwheel')" >
</div>

106個のイベントハンドラを設定しましたが、この input 要素上でマウスを動かす等の操作をしても予想通りイベントハンドラが呼び出されることはありませんでした。

では、次のようなHTMLの場合はどうでしょうか。

<!DOCTYPE html>
<head>
<script>
document.addEventListener('DOMContentLoaded', () => {
  document.querySelector('button').addEventListener('click', () => {
    document.querySelector('[inert] input').requestFullscreen()
  })
})
</script>
</head>
<body>
<div inert>
  <input type="text" onabort="console.log('onabort')" ... onkeydown="console.log('onkeydown')" ... >
</div>
<button>fullscreen</button>
</body>

inert属性の指定されたdiv要素やその配下のinput要素は先ほどの例と同じですが、 <button> が押されたときにそのinput要素をフルスクリーン表示するコードが追加されています。 実際に動かしてボタンをクリックすると、画面いっぱいに input 要素が表示(つまり画面全体が真っ白になる)されキー押下に合わせてなんとキー操作で文字の入力もできますし、フルスクリーンを解除してDevToolsのコンソールを見てみると大量のイベントハンドラ実行のログが記録されています!

DevTools のコンソール

どうやら、フルスクリーン表示された要素は親要素のinert属性の制約を受けないようで、イベントハンドラも実行されるようです。 inert 属性の仕様 もざっと読んでみたのですが、フルスクリーン表示のときに不活性化された要素をどのように扱うかについては特に記述されていないようでした。

XSSでの攻撃につなげられるの?

inert属性によって不活性化されイベント発火が抑制された要素であっても、フルスクリーン表示させることでイベントを発火させられることがわかりました。ではこの挙動を用いてXSSでの攻撃可能性を上げることができるかというと、そもそも任意の要素をフルスクリーン表示させられる状況というのが考えにくいため、残念ながら否となります。

というわけで、なかなか新しい攻撃方法の発見とまでは至りませんでしたが、ブラウザーに新しい機能が実装されるたびにこういった実験をするのは楽しいですね! もしフルスクリーン表示をせずにinert属性の不活性化をバイパスする方法を見つけた方は、ぜひこっそり教えてください!

*1:HTMLInputElementの持つonで始まるプロパティを機械的に列挙しています