ログインのあとに元のページへ戻す、といった処理では、戻り先の 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 を使う

参考