攻撃者は、クライアントサイド・パストラバーサルを応用すれば Self XSS を XSS に昇格できるらしいけど、どういう意味?
ここで言う「クライアントサイド・パストラバーサルを応用して Self XSS を XSS に昇格」は、だいたい次の意味です。
「昇格」というのは、本来は被害者の能動的な協力が必要だった“コード実行”を、リンク1つ/閲覧だけで起こせる形に変える、ということです。
鍵は「クライアント側で ../ などを混ぜて、読み込むべき“スクリプト/HTML/テンプレ/JSON”の場所を攻撃者が変えられる」点です。
例(イメージ):
import("/widgets/" + name + ".js")fetch("/templates/" + page + ".html").then(el.innerHTML = ...)fetch("/i18n/" + lang + ".json") → 文字列をDOMに反映この name/page/lang を URLパラメータ等から取っていて、正規化・許可リストがないと、../ でパスをずらせます。
たとえば
/i18n/en.json を読むはずが、lang=../../uploads/evil みたいにして /uploads/evil.json を読ませる/widgets/foo.js を読むはずが、name=../user-content/evil みたいにして ユーザーがアップロードできる場所のJS相当を読ませるここで重要なのは、攻撃者が“被害者に何か貼らせる”必要がなく、URLを踏ませるだけで読み込み先を変えられることです。
import() や <script src=...> に到達できる → JSとして実行されうるfetch(...).then(html => target.innerHTML = html) → HTMLとして解釈され、イベント属性等でXSSになりうるinnerHTML に入れる等の扱いがあるとDOM XSS化しうる結果として、攻撃者は
という「被害者操作不要のXSS」へ持ち込みます。これを「Self XSS → XSSへ昇格」と表現します。
この手の話で「Self XSS」と呼ばれているものは、厳密には次のどちらかが多いです。
(A) もともと“コンソール貼り付け”が必要だったタイプ
例:開発者ツールで localStorage.lang="../../uploads/evil" を手で入れないと発火しない
→ これが、パストラバーサルにより URLパラメータだけで同じ状態を作れると、Self性が消えてXSSになる。
(B) “自分のコンテンツが自分にだけ効く”タイプ
例:自分のアップロードしたファイル名/設定が、自分の画面でしか参照されないのでSelf扱い
→ しかしパストラバーサルで 他人の画面の読み込み先も誘導できると、他者に波及してXSSになる。
クライアントサイド・パストラバーサルによって「どの資産を読み込む/表示するか」を攻撃者がURLだけで操作でき、かつその資産がアプリの都合で“実行される”経路に乗ると、被害者の協力が必要だったSelf XSSが、リンク踏ませるだけのXSSに変わる、という意味です。
もし、対象が「SPAでのテンプレ読み込み」「i18n JSONの動的ロード」「dynamic import」「Electron」など、どのパターンか分かれば、典型コード例と“どこが境界突破点になるか(allowlist/正規化/innerHTML等)”をそのケースに合わせて整理できます。
え、わからない
- 本来
/i18n/en.jsonを読むはずが、lang=../../uploads/evilみたいにして/uploads/evil.jsonを読ませる
これってユーザーが入力フォームとかに /uploads/evil.json を入れないといけないってことだよね?
入力フォームのあるページにアクセスしただけで /uploads/evil.json にアクセスさせるわけじゃないよね?
「入力フォームに手で打ち込まないと無理なのでは?」という疑問はもっともで、ポイントはここです:
/uploads/evil.json を取りに行くことがある以下、成立する典型パターンを具体的に分けて説明します。
SPAや一部の画面は、URLから状態を復元します。
例:アプリがこういう実装だとします:
const lang = new URL(location.href).searchParams.get("lang") || "en";
fetch(`/i18n/${lang}.json`).then(/* ... */);
このとき攻撃者は被害者に次のリンクを踏ませるだけです:
https://example.com/app?lang=../../uploads/evil
するとブラウザは(少なくとも要求としては):
GET /i18n/../../uploads/evil.jsonを投げます。サーバやフレームワークの正規化次第で、実質
GET /uploads/evil.jsonを取りに行けてしまうことがあります。
ここではフォーム入力は不要で、「リンクを開いただけ」で成立します。
#(ハッシュ)由来(これもリンクだけで発火)SPAは /#/page みたいなハッシュルーティングを使います。
const page = location.hash.slice(2); // "#/xxxx" を読む
fetch(`/templates/${page}.html`).then(r => r.text()).then(html => {
container.innerHTML = html;
});
攻撃者リンク:
https://example.com/#/../../uploads/evil
→ ページ表示だけで fetch("/templates/../../uploads/evil.html") が走り得ます。
「コンソールで localStorage.lang=... を入れたら変なファイルを読めた」というのは、たしかに最初は Self XSS っぽいです。
でも、もしアプリに
みたいなのがあると、攻撃者はリンク一発で 被害者のlocalStorageに値を保存させられます。
例:
const lang = new URL(location.href).searchParams.get("lang");
if (lang) localStorage.setItem("lang", lang);
const l = localStorage.getItem("lang") || "en";
fetch(`/i18n/${l}.json`);
攻撃者が ?lang=../../uploads/evil のリンクを踏ませる
→ 被害者は一切入力してないのに、次回以降も勝手にそれを読むようになる
こういう「URL→保存→読み込み」の導線があると、“自分で入れた場合だけ起きる”が崩れてSelf性が消えます。
もちろん実装によっては、あなたの言う通り
みたいな作りもあります。その場合、それ単体だと「被害者が入力しないと起きない」のでXSSとしては弱くなりがちです。
ただ現実には、次のどれかがあると「入力不要」に寄ります:
onchange/oninput で即ロードする/uploads/evil.json を「読ませる」だけでは、まだXSSにならないあなたの疑問の延長で誤解しやすい点ですが、
XSSになるのは、そのJSON等が後段で
innerHTML に入るimport() や <script src> として実行されるなど、「実行される経路」に乗っているときです。
次のどれに近いですか?
lang/page/template みたいな値は URLパラメータ or ハッシュから読んでますか?fetch() / import() / innerHTML などをしていますか?該当するコード断片(URLから読むところと、fetch/import/innerHTMLしてるところ)を貼ってくれれば、「入力フォームが必要か」「リンクだけで行けるか」「XSSまで行く条件は何か」をあなたのケースで具体的に整理します。