🤦‍♂️

target='_blank'の脆弱性と直面した問題について

2023/10/22に公開1

概要

<a href="https://~~~" target="_blank">別タブで遷移</a>

を使用する際に直面した問題と、target="_blank"の脆弱性についてまとめます。

target="_blank"の脆弱性について

問題のコード

'use client';
const PageA = () => {
  return (
    <div>
      <button
        onClick={() => {
          window.open('/pageB', '_blank');
        }}
      >
        ページB</button>
    </div>
  );
};
export default PageA;

buttonを押した際にwindow.open('/pageB', '_blank');でpageBに遷移しています(外部サイトの場合も考えられます。)。
このコードの脆弱性としては、タブナビング(Tabnabbing)攻撃が成立してしまう危険性があるというところです。

タブナビング(Tabnabbing)攻撃とは

タブナビング攻撃とはフィッシング攻撃の一種であり、ユーザーの知らないうちにタブがフィッシングサイトに化けさせユーザーの機密情報などを盗み出そうとする攻撃である。
target="_blank"で別タブ遷移をさせるサイトでのタブナビング攻撃の流れは以下のようになります。

  1. ページAから別タブ遷移でページB(外部サイト)に遷移する
  2. ページBで元ページ(ページA)のwindowを書き換えるようなコードがある
  3. ユーザーはページBを見た後、元ページ(ページA)に戻るがページAはフィッシングサイトに書き換えられている
  4. 気づかずにページA(フィッシングサイト)を使用してしまう

コード例を用いて説明します。

  1. ページAから別タブ遷移でページB(外部サイト)に遷移する
pageA
<div>
  <button
    onClick={() => {
      //外部サイトに遷移
      window.open('http://localhost:3000/pageB', '_blank');
    }}
  >
    ページB</button>
 </div>
  1. ページBで元ページ(ページA)のwindowを書き換えるようなコードがある
pageB
useLayoutEffect(() => {
    if (window.opener) {
      // 遷移元のwindowを書き換える
      window.opener.location = 'http://localhost:3000/akui-page';
    }
}, []);
return (
    <div>
      <div>ページBです</div>
    </div>
);

window.opener.location = 'http://localhost:3000/akui-page';
で遷移元ページを書き換えることができてしまいます。

  1. ユーザーはページBを見た後、元ページ(ページA)に戻るがページAはフィッシングサイトに書き換えられている

もし、元のサイトとそっくりなUIでフィッシングサイトが作られていたとしたら、気づかずに操作を続けてしまうユーザーもいると考えられます。

タブナビング攻撃の対策

  1. rel="noopener"を指定して、ターゲットリソースへ移動する際、別スレッドでターゲットリソースを処理するようにし、開いた元の文書へのアクセスを新しい閲覧コンテキストに許可しないことをブラウザーに指示する。
    rel="noopener"を指定することで開かれたwindowではwindow.openerはnullを返すようになる。
    が、HTTPのRefererヘッダーは(noreferrerを同時に使用しない限り)提供される。
pageA
window.open('http://localhost:3000/pageB', '_blank', 'noopener');
pageB
useEffect(() => {
    if (window.opener) {
      // 遷移元のwindowを書き換える
      window.opener.location = 'http://localhost:3000/akui-page';
    }
    // リファラ情報は参照できる
    console.log(document.referrer);
  }, []);
  1. リファラ情報も参照させたくない場合はrel="noreferrer"を指定することでリファラ情報を送信することを拒否できる。

window.open('https://~~~~~', '_blank', 'noopener noreferrer');で直面した問題について

window.openで別タブ遷移をしようとした際、safariではポップアップブロックされてしまい、ボタンを押しても何も起こらないという事象が発生してしまいます(safari以外のブラウザでも同事象が発生する可能性があります)。
正確には以下のような処理がポップアップブロックの対象になります。

  1. ボタンを押す
  2. onClickで非同期処理をする
  3. 非同期処理が終わった後にwindow.open('https://~~~~~', '_blank', 'noopener noreferrer');で別タブ遷移させる
  4. ポップアップブロックが発生する

問題のコード

const promiseFunc = () => {
    // 非同期処理
    return new Promise((resolve) => setTimeout(resolve, 3000));
  };
  return (
    <div>
      <button
        onClick={async () => {
          await promiseFunc();
          //外部サイトに遷移
          window.open('http://localhost:3000/pageB', '_blank', 'noopener noreferrer');
        }}
      >
        ページB</button>
    </div>
  );

ポップアップブロックとは

ポップアップブロックは、ユーザーの直接のアクション(ボタンを押す、リンクをクリックする etc)によってのみポップアップを許可するブラウザの機能である。
この機能は、頻繁にポップアップウィンドウを表示する広告などの迷惑なコンテンツや悪意のあるコンテンツをブロックするための仕組みであり、safariではデフォルトで有効になっている。
例えば、ポップアップブロックがない場合、以下のようなこともできてしまう。

<button
  onClick={async () => {
    //外部サイトに遷移
    while (true) {
      window.open('http://localhost:3000/pageB', '_blank', 'noopener noreferrer');
    }
   }}
>
  ページB</button>

chromeでもこのようなコードの場合でも、ポップアップウィンドウは一つのみ表示されるようになっているが、ブラウザによってポップアップブロックするべきコンテンツかどうかの判定が異なる。

chromeの場合、window.openの前に非同期処理を行なってもポップアップブロックはされないが、
safariの場合は、非同期処理を遷移前に行うことで、ユーザーの直接のアクションとみなされないようになってしまうため、ポップアップブロックが発生する。

ポップアップブロックの対策

この場合の対策としては、ポップアップブロックされた場合は同タブの遷移に切り替える処理が必要であると考えられる。

window.openはブロックされた場合nullを返すので

<button
  onClick={async () => {
    await promiseFunc();
    //外部サイトに遷移

    window.open('http://localhost:3000/pageB', '_blank', 'noopener noreferrer') ??
      router.push('http://localhost:3000/pageB');
  }}
>
   ページB</button>

としたいところではあるが、'noopener noreferrer'を指定してしまうと、全く別のスレッドで別タブが処理されてしまうので、たとえ別タブ遷移が成功したとしてもwindowを参照することができずnullが返却される。

そのため、やむをえないが

window.open('http://localhost:3000/pageB', '_blank') ?? router.push('http://localhost:3000/pageB')

'noopener noreferrer'を外すことになるだろう。

参考
mdn rel=noreferrer
mnd rel=noopener
ios safariでwindow.openできない時の対処法について

Discussion