コメント欄に書いた文字が、そのまま画面に表示される。よくある機能です。ここで文字ではなくHTMLのタグを書かれると、それがタグとして動いてしまうことがあります。これがXSSです。
今回は同じ入力を3か所に埋めて試しました。本文・画像の説明文・リンク先の3つです。結果を先に言うと、エスケープしたはずのリンクに javascript: がそのまま残っていました。埋める場所によって必要な処理が違うからです。
起きているのは、書き込まれた文字がHTMLの一部として読まれることです。文字を表示する欄が、そのまま部品を追加できる欄になってしまいます。
XSSは、利用者が書いた内容を画面に出すときに、それが文字ではなくHTMLとして解釈されることで起きます。コメント欄や名前の表示など、入力をそのまま出す場所が対象になります。
「こんにちは」と書き込まれた場合、画面にはその文字が出るだけです。ここまでは意図した動きで、問題はありません。
ところが <img src=x onerror=alert(1)> のような文字列を書き込まれると、ブラウザはこれを画像の指定として読みます。画像の読み込みは失敗しますが、失敗したときに動く処理の部分が実行されます。
この例では警告を出すだけですが、同じ場所で他の処理も書けます。ログイン状態を保つ情報を盗む、勝手に操作を実行する、といったことが他の利用者のブラウザ上で起こります。
OWASPは、変数をHTMLの中に安全に入れるには、その変数にHTMLの実体参照への変換を行うよう示しています。つまり< を < のような別の表記に置き換えて、タグとして読まれないようにします。
入力を出力する場所が3種類あり、必要な処理はそれぞれ違う。同じ変換で全部は守れない。
XSSで被害を受けるのは、書き込んだ本人ではなくそのページを見た他の利用者です。ここが分かりにくい点だと思います。掲示板に仕掛けを書き込んでおき、それを見た人のブラウザで動く、という順序になります。
In order to add a variable to a HTML context safely to a web template, use HTML entity encoding for that variable.原文OWASP Cheat Sheet Series「Cross Site Scripting Prevention」 この内容の有効期限2027-02-17
同じ入力を本文・属性・URLの3か所に埋めて試しました。本文と属性は変換で無害になりましたが、URLだけは通り抜けます。
XSSの対策は「エスケープすれば終わり」と説明されがちですが、実際に3か所へ埋めて出力を見比べました。
下のコードは読み飛ばして大丈夫です。やっていることは、同じ変換関数を3か所に使っただけです。
const escapeHtml = (s) =>
String(s).replace(/[&<>"']/g, (c) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
})[c]);
const renderSafe = (v) => `<div class="comment">${escapeHtml(v)}</div>`;
const renderAttrSafe = (v) => `<img src="/avatar.png" alt="${escapeHtml(v)}">`;
const renderHrefEscaped = (v) => `<a href="${escapeHtml(v)}">プロフィール</a>`;
// URLはスキームを確かめる。変換では javascript: を止められない
const renderHrefSafe = (v) =>
/^https?:\/\//i.test(v) ? `<a href="${escapeHtml(v)}">プロフィール</a>`
: '<a href="#">(不正なURL)</a>';
--- 入力をHTML本文に埋める --- 対策なし: <div class="comment"><img src=x onerror=alert(1)></div> エスケープあり: <div class="comment"><img src=x onerror=alert(1)></div> --- 同じ入力を属性値に埋める --- エスケープあり: <img src="/avatar.png" alt="<img src=x onerror=alert(1)>"> --- URLとして javascript: を渡す --- エスケープのみ: <a href="javascript:alert(1)">プロフィール</a> スキームも確認: <a href="#">(不正なURL)</a>
残りました。href の中身が javascript:alert(1) のままです。エスケープ関数は通っているのに、変換すべき文字が1つも含まれていないので、何も変わらずに出てきました。
変換が守っているのは「文字がタグとして読まれること」だけだからです。javascript:alert(1) には山括弧も引用符も入っていません。文字としては完全に無害で、危険なのは行き先そのものです。
つまりURLについては、文字を無害にする処理ではなく、行き先が許されるものかを確かめる処理が要ります。上のコードでは、先頭が http:// か https:// であることだけを条件にしています。
OWASPのチートシートも、信頼できないデータをさまざまな場所で安全に描画する方法として、場所ごとに分けて示しています。1つの変換関数で全部を守る、という発想にならないよう作られています。
These snippets of HTML demonstrate how to render untrusted data safely in a variety of different contexts.原文OWASP Cheat Sheet Series「Cross Site Scripting Prevention」 この内容の有効期限2027-02-17
確認は2種類です。コメント欄にタグを入れて表示が崩れないか。URL欄に javascript: を入れて弾かれるか。この2つで大半が見つかります。
XSSの確認に特別な道具は要りません。実際に入力してみるのが確実です。ただし入れる値と見る場所を分けて考える必要があります。
<img src=x onerror=alert(1)> を入れて保存するjavascript:alert(1) を入れて保存するこの2つ目が、前の節で通り抜けたパターンです。本文の対策だけ済ませて安心している場合に見つかります。
対策が済んだと言えるのは3条件すべて。URLのスキーム確認が最も抜けやすい。
通信を監視して攻撃を弾くしくみ(WAF)もありますが、OWASPはXSSの防止にWAFは勧められないと明記しています。特に、ブラウザ内だけで完結する種類のXSSには効きません。
入力を信用しないという点ではSQLインジェクションの記事と同じ構図です。あちらは入力がSQLの命令として読まれる問題で、こちらは入力がHTMLとして読まれる問題です。
この確認が効くのは実施した時点までです。入力を表示する場所は機能を足すたびに増えます。手作業の確認は最初の棚卸しに使い、以降はテストコードに任せるのが現実的です。タグを入れて出力を比べるだけなので、自動テストにも落としやすい部類です。
WAFs are not recommended for preventing XSS, especially DOM-Based XSS.原文OWASP Cheat Sheet Series「Cross Site Scripting Prevention」 この内容の有効期限2027-02-17
<img src=x onerror=alert(1)> を入れて、画像の壊れた表示が出ないか確かめる同じ課題を持つ会社にとって、動いている設定は「作る時間」を買えるということです。ServiceDockは自作のワークフローやテンプレートを出品できるマーケットプレイスです。手数料や出品の流れは出品者向けページにまとまっています。
出品の仕組みを見る