Intigriti Challenge 0526: Username から DOM Clobbering で tracker loader に到達する Stored XSS リンクを取得 Facebook × Pinterest メール 他のアプリ 5月 26, 2026 Intigriti Challenge 0526 で、accepted された display-name XSS とは別に、username から `PixelAnalyticsConfig` を DOM clobbering して、analytics 用の script loader に到達する経路を見つけた。 この経路では、`onerror` や `ontoggle` のようなイベント属性を使わない。 HTML として挿入された `` 要素で `window.PixelAnalyticsConfig` を作り、アプリ内の tracker loader に外部 JavaScript を読み込ませる。 対象は次のページ。 ```text https://challenge-0526.intigriti.io/challenge ``` ## 使った payload 登録時の username に次を入れる。 ```html u ``` `YWxlcnQoMSk7` は Base64 で、デコードすると次になる。 ```js alert(1); ``` `httpbin.org/base64/YWxlcnQoMSk7` は、レスポンス本文として `alert(1);` を返す。 この URL を script として読み込ませる。 ## 再現手順 まず、新規ユーザー登録時に username として payload を指定する。 ```html u ``` 登録後、ログインした状態で次の URL を開く。 ```text https://challenge-0526.intigriti.io/challenge#testimonials ``` Testimonials 画面を開くと、nav に username が描画される。 その後、analytics loader が `window.PixelAnalyticsConfig` を読み、外部 script が読み込まれる。 Chrome では `alert(1)` が表示された。 ## 原因 この経路は、次の2つが組み合わさって成立する。 1つ目は、username が nav の描画時に HTML として挿入される点。 ```js nav.innerHTML = ... ``` 実際のコードでは、登録時の username が nav に入り、その中の HTML が解釈される。 そのため、username に含めた `` 要素が DOM 上に作られる。 2つ目は、Testimonials 側で次のような tracker loader が動く点。 ```js let config = window.PixelAnalyticsConfig || { enabled: false, scriptUrl: '/js/mock-tracker.js' }; if (config.enabled) { let s = document.createElement('script'); s.src = config.scriptUrl; document.body.appendChild(s); } ``` このコードは `window.PixelAnalyticsConfig` を信頼している。 ここに DOM clobbering で作った値を読ませる。 ## DOM clobbering の流れ payload には、同じ `id=PixelAnalyticsConfig` を持つ `` 要素が2つある。 ```html ``` ブラウザでは、`id` を持つ要素が `window.` として参照できる場合がある。 同じ id を持つ要素が複数あるため、`window.PixelAnalyticsConfig` は単一要素ではなく collection のような値になる。 その collection に対して、`name` 属性を使った参照ができる。 ```js config.enabled config.scriptUrl ``` このとき、次のように解決される。 ```js config.enabled // -> config.scriptUrl // -> ``` `config.enabled` は要素オブジェクトなので truthy になる。 そのため、`if (config.enabled)` の中に入る。 その後、`config.scriptUrl` が `s.src` に代入される。 ```js s.src = config.scriptUrl; ``` `config.scriptUrl` は `` 要素なので、文字列として扱われると `href` の URL になる。 結果として、次の script が追加される。 ```html ``` このレスポンスは `alert(1);` を返すため、ページ上で JavaScript が実行される。 ## display-name XSS との違い 先に accepted されたレポートでは、display name が `innerHTML` に入る sink を使った。 ```js nameDiv.innerHTML = t.user_name; ``` payload は display name に保存し、Community Feed の描画時に event handler を発火させた。 一方、この経路では display name を使わない。 登録時の username が nav に入るところから始まり、最終的には Testimonials 側の tracker loader を使う。 違いを整理するとこうなる。 | 項目 | accepted された経路 | 今回の経路 | |---|---|---| | 入力箇所 | display name | username | | 最初の sink | testimonial author の `innerHTML` | nav rendering の `innerHTML` | | 実行方法 | event handler | DOM clobbering + script loader | | `alert(1)` の呼び出し | HTML イベント属性 | 外部 script | | 必要な gadget | 不要 | `PixelAnalyticsConfig` loader | 今回の経路は、単に HTML イベント属性を踏ませるのではなく、アプリ側にある loader を使って実行に到達する。 その点で、別のバグとして説明しやすい。 ## testimonial body からの関連パターン 同じ `PixelAnalyticsConfig` gadget は、testimonial body からも使えた。 testimonial の本文に次を投稿する。 ```html ``` 本文は次のように挿入される。 ```js textDiv.innerHTML = DOMPurify.sanitize(t.content); ``` DOMPurify は、この payload の `` 要素を残す。 その後の tracker loader が `window.PixelAnalyticsConfig` を読むと、username 版と同じ流れで外部 script が読み込まれる。 ## まとめ この経路では、username に保存した HTML が nav に挿入され、`window.PixelAnalyticsConfig` を DOM clobbering する。 その後、Testimonials 側の tracker loader が clobbering された config を読み、外部 script として `alert(1);` を実行する。 payload 自体には event handler がない。 実行はアプリ内の analytics loader によって起きる。 そのため、display-name の `innerHTML` sink を使う XSS とは別の経路として扱える。 `window.PixelAnalyticsConfig` のようなグローバル参照に設定を置く場合、DOM の named property と衝突しないようにする必要がある。 外部 URL を script として読み込む設計自体も危険なので、許可した URL だけを使う方がよい。 DOMPurify を使う箇所では、許可する属性も絞る。 今回のように `id` と `name` が残ると、HTML としては安全に見えても、後段の JavaScript と組み合わさって DOM clobbering につながることがある。 締め切りに合わせるために淡白な口調になっちゃった;; 楽しいチャレンジをありがとう!intigrity! リンクを取得 Facebook × Pinterest メール 他のアプリ コメント
コメント
コメントを投稿