一覧SD-03-1 · XSSと出力の安全性

XSSとDOM操作

入力元と出力先をたどり、データが要素やイベント処理へ変わる場所を説明します。

説明

XSSでは、外部から来た値をページがコードとして解釈することが問題になります。URLから受け取る値、保存済みの投稿、ブラウザ内で読み取る値など、入口が違っても、最後にHTMLやイベント処理として扱う箇所を確認する必要があります。

ページ内の表示領域へ、固定のボタン文字列を渡します。URLパラメーター、保存値、fragmentから届く経路を比較し、innerHTMLとtextContentで生成されるDOMとクリック後の状態を確かめます。

入力元と出力先をたどり、データが要素やイベント処理へ変わる場所を説明します。

1URLの値を取り出す

試料はクリックするとページ内のdata-demoだけをranへ変えるボタンです。最初は単なる文字列として扱います。

window.host = document.getElementById("scratch"); window.payload = `<button onclick="document.body.dataset.demo='ran'">Open</button>`; window.query = new URLSearchParamsURLパラメーターを組み立てる({q:payload}); console.log(query.get("q")受領した値を取得する);

URLの符号化を戻すとHTMLに見える文字列が得られます。この時点ではボタンを作っていません。

2HTMLとして解釈する出力先

innerHTMLへ代入します。文字列中のタグと属性がDOMへ変換されます。

document.body.dataset.demo = "idle"; host.innerHTML =入力をHTMLとして解釈する query.get("q"); console.log(host.children.length,host.firstElementChild.tagName,host.firstElementChild.getAttribute("onclick")生成されたイベント属性を確認する); console.log(document.body.dataset.demo);

BUTTON要素とonclick属性ができました。クリックする前の状態はidleですが、入力した属性がイベント処理として登録されています。

3入力由来のイベントが実行される

作られたボタンへクリックを送ります。

host.querySelector("button").click()生成されたボタンのイベントを発生させる; console.log(document.body.dataset.demo);

ranへ変わりました。単なる表示用文字列のつもりで受けた値が、このページ上の処理として動いています。

4保存を経由して同じ値を表示する

この試料の保存先にはlocalStorageを使います。サーバーのDBに保存する場合も、保存済みという理由で出力時の信頼性が上がるわけではありません。

localStorage.setItem投稿の保存を表す("lbox-sd03-note",payload); document.body.dataset.demo = "idle"; host.innerHTML =保存値をHTMLへ渡す localStorage.getItem保存した値を読み戻す("lbox-sd03-note"); host.querySelector("button").click(); console.log(document.body.dataset.demo);

保存後に読み戻した値でも同じイベントが動きました。入力を保存する処理と、HTMLとして出力する処理を分けて追います。

5fragmentからDOMへ渡す

URLの#以降はfragmentです。この値をブラウザ側で取り出し、同じ出力先へ渡す経路を比較します。

window.sampleUrl = new URL("https://example.test/#"+encodeURIComponent(payload)); window.fromFragment = decodeURIComponent(sampleUrl.hash.slice(1)fragmentを取り出す); host.innerHTML = fromFragmentブラウザ側の値をHTMLへ渡す; console.log(fromFragment === payload,host.querySelectorAll("button").length);

同じボタン要素ができました。fragment自体をサーバーへ送らなくても、ブラウザ側のDOM操作が解釈の入口になります。

6文字列として表示する

出力先をtextContentへ変更します。

document.body.dataset.demo = "idle"; host.textContent = payload入力を文字として表示する; console.log(host.children.length,host.textContent === payload,document.body.dataset.demo);

子要素0で、文字列全体が表示用テキストになります。イベント属性は生成されずidleのままです。

7保存値にも同じ出力方法を適用する

保存した文字列を、文字として読み返します。最後にこの試料の保存キーだけを削除します。

host.textContent =保存した値も文字として扱う localStorage.getItem("lbox-sd03-note"); console.log(host.querySelectorAll("button").length,host.textContent); localStorage.removeItem試料の保存値を消す("lbox-sd03-note");

入口がURLでも保存値でも、HTMLが不要な表示ならtextContentを使えます。入力経路だけでなく、最終的な解釈先を修正します。

まとめ

URL・保存値・fragmentのいずれから来ても、HTMLとして解釈する出力先を確認します。

入力中のイベント属性がDOMへ生成されると、ページの処理として動きます。

HTMLが不要な場所では、文字列を文字として表示するAPIを使います。

操作履歴と解説を振り返れます。

JavaScript / ブラウザのDOM・CSP / 固定のHTML試料

1
window.host = document.getElementById("scratch"); window.payload = `<button onclick="document.body.dataset.demo='ran'">Open</button>`; window.query = new URLSearchParams({q:payload}); console.log(query.get("q"));
<button onclick="document.body.dataset.demo='ran'">Open</button>
2
document.body.dataset.demo = "idle"; host.innerHTML = query.get("q"); console.log(host.children.length,host.firstElementChild.tagName,host.firstElementChild.getAttribute("onclick")); console.log(document.body.dataset.demo);
1 BUTTON document.body.dataset.demo='ran'
idle
3
host.querySelector("button").click(); console.log(document.body.dataset.demo);
ran
4
localStorage.setItem("lbox-sd03-note",payload); document.body.dataset.demo = "idle"; host.innerHTML = localStorage.getItem("lbox-sd03-note"); host.querySelector("button").click(); console.log(document.body.dataset.demo);
ran
5
window.sampleUrl = new URL("https://example.test/#"+encodeURIComponent(payload)); window.fromFragment = decodeURIComponent(sampleUrl.hash.slice(1)); host.innerHTML = fromFragment; console.log(fromFragment === payload,host.querySelectorAll("button").length);
true 1
6
document.body.dataset.demo = "idle"; host.textContent = payload; console.log(host.children.length,host.textContent === payload,document.body.dataset.demo);
0 true idle
7
host.textContent = localStorage.getItem("lbox-sd03-note"); console.log(host.querySelectorAll("button").length,host.textContent); localStorage.removeItem("lbox-sd03-note");
0 <button onclick="document.body.dataset.demo='ran'">Open</button>

演習完了