ログインのあとに元のページへ戻す、といった処理では、戻り先の URL をパラメーターで受け取ることがよくあります。このとき、受け取った値を確かめずに location.href へ代入すると、クロスサイトスクリプティング (XSS) の脆弱性になります。
危ない書き方
// ?next=/mypage のような値で、戻り先を受け取る
const next = new URLSearchParams(location.search).get("next");
location.href = next;
next に /mypage が入っていれば、ふつうにページを移動するだけです。ところが、攻撃者が次のような URL を用意して、利用者に開かせたとします。
https://example.com/login?next=javascript:alert(document.cookie)
location.href に javascript: で始まる URL を代入すると、ブラウザーはページを移動する代わりに、その文字列を JavaScript として実行します。しかも実行されるのは example.com のページの中なので、そのサイトのページを書き換えたり、利用者の操作になりすましたりできてしまいます。サーバーを通らずにブラウザーの中だけで起きるため、「DOM ベースの XSS」と呼ばれます。
また、next=https://attacker.example/ のように外部のサイトを指定されると、正規のサイトから偽のサイトへ誘導されます (オープンリダイレクト)。
URL を確かめてから移動する
受け取った値は、URL オブジェクトで読み取ってから、移動してよい URL かを確かめます。戻り先を同じサイトの中に限るなら、オリジン (スキーム・ホスト名・ポート番号の組) を比べるのが確実です。
function redirectTo(value, fallback = "/") {
try {
// 相対 URL は、今のページのオリジンを基準に読む
const url = new URL(value, location.origin);
if (url.origin === location.origin) {
location.href = url.href;
return;
}
} catch {
// URL として読めない値は使わない
}
location.href = fallback;
}
redirectTo(new URLSearchParams(location.search).get("next"));
javascript:alert(1)のような値は、オリジンが"null"になるので、同じサイトのオリジンと一致しません//attacker.example/のようにスキームを省いた URL も、URLで読むと外部のオリジンになるので、はじかれます- 値が無いときや読めないときは、決めておいたページ (
fallback) へ移動します
文字列が / で始まるかどうかだけで判断すると、//attacker.example/ のような値を見落とします。自分で文字列を調べるより、URL に読ませてから比べるほうが安全です。
ほかの場所でも同じことに気をつける
URL の値をそのまま使って危ないのは、location.href だけではありません。
a要素のhrefやiframeのsrcに、確かめていない URL を設定するinnerHTMLに、パラメーターの値を含む文字列を代入する (文字として表示するだけならtextContentを使う)eval()やsetTimeout()に文字列を渡す
あわせて、Content Security Policy (CSP) でスクリプトの実行元を制限しておくと、万一このような不具合があっても被害を小さくできます。
まとめ
location.hrefにjavascript:で始まる値を代入すると、そのページの中でスクリプトが実行される- 外から受け取った URL は、
new URL()で読み取り、オリジンなどを確かめてから使う - 文字として表示するだけなら
innerHTMLではなくtextContentを使う