コムセント 技術情報

  1. TOP
  2. コムセント 技術情報
  3. 外部リンク用クッションページを HTML + JavaScript のみで作成する

外部リンク用クッションページを HTML + JavaScript のみで作成する

Web サービスによっては、外部リンク(そのサービスのドメインと違うドメインのリンク)をクリックした際、クッションページが挟まれることがあります。

このクッションページは、

  • リンク先のページが外部リンクであることを明示する
  • リンク先のページが安全であるかどうかをユーザに確認して貰う
  • 機密情報をリファラ情報としてリンク先に送信しないようにする
  • どの外部リンクがクリックされたかをトラッキングする

などの目的で利用されます。

多くの場合はサーバーサイドでクッションページを生成していますが、実は HTML + JavaScript のみで作成できるのではないか? と思い立ち、作ってみました。

実際のコード(リンクが設置されているページ用)

HTML は適当で良いのですが、リンクが設置されているページとは別にクッションページが必要です。

リンクが設置されているページ用の JavaScript は以下です。

// 元ページ用の JavaScript
// クッションページの URL を定義
const cushionPageUrl = 'blog/cushion?target=';

/**
 * 指定された要素内の全ての a 要素における href 属性が外部リンクの場合、クッションページへのリンクに置換する
 * @param links {Array<HTMLAnchorElement>} a 要素の配列
 */
const rewriteLinksToCushionLinks = (links) => {
  // links が HTMLCollection or NodeList でない場合は何もしない
  if ( ! (links instanceof HTMLCollection || links instanceof NodeList)) {
    return;
  }

  // 内部リンク判定用の正規表現を定義
  const internalLinkPattern = new RegExp(`^${location.origin}`);

  // 全ての a 要素に対して判定、置換処理を実行
  links.forEach(link => {
    // a 要素でない場合は何もしない
    if ( ! (link instanceof HTMLAnchorElement)) {
      return;
    }

    // href 属性が空の場合は何もしない
    if ( ! link.href) {
      return;
    }

    // 既に変換済みのリンクの場合は何もしない
    if (link.href.indexOf(cushionPageUrl) === 0) {
      return;
    }

    // href 属性がページ内リンク or 内部リンクの場合は何もしない
    if (internalLinkPattern.test(link.href)) {
      return;
    }

    // href 属性が外部リンクの場合はクッションページへのリンクに置換
    link.href = cushionPageUrl+encodeURIComponent(link.href);

    // target 属性を _blank に設定
    link.target = '_blank';
  });
};

// ページ読み込み完了時に実行
document.addEventListener('DOMContentLoaded', () => {
  // 全ての a 要素に対して処理を実行
  // rewriteLinksToCushionLinks(document.querySelectorAll('a'));

  // class="target-links" を持つ最初の要素内の全ての a 要素に対して処理を実行
  rewriteLinksToCushionLinks(document.querySelector('.target-links').querySelectorAll('a'));
});

const cushionPageUrl = 'blog/cushion?target=';

の部分は、クッションページの URL を指定します。

?target=

という部分はそのまま固定です。

実際にこの記事にも仕込まれているので、動作を確認してみて下さい。

https://www.google.com/search?q=%E3%82%AF%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8

実際のコード(クッションページ用)

一方で、クッションページ側には以下の JavaScript を設置します。

// クッションページ用の JavaScript
document.addEventListener('DOMContentLoaded', () => {
    // クエリパラメータからリダイレクト先 URL を取得
    const url = new URLSearchParams(location.search).get('target');
    
    // リダイレクト先 URL が取得できない場合は何もしない
    if ( ! url) {
      return;
    }

    // javascript: が含まれる場合は何もしない
    if (url.indexOf('javascript:') !== -1) {
    return;
  }
    
    // リダイレクト先 URL にリダイレクト
    const anchor = document.createElement('a');
    anchor.setAttribute('href', decodeURIComponent(url)
      .replace(/</g, '<')
      .replace(/>/g, '>')
      .replace(/"/g, '"')
      .replace(/'/g, "'"));
    anchor.textContent = url;
    document.getElementById('link-url').appendChild(anchor);
});

リンク先として a タグを生成したい親要素に id="link-url" を設定しておきます。

以上で実際に動作するかと思います。

コードの解説

元ページ用の JavaScript

rewriteLinksToCushionLinks 

関数で受け取った a 要素の href 属性を置換しているのですが、

  • ページ内リンク(同じページに存在する id が指定されているリンク)
  • 内部リンク(同じドメイン内のリンク)

は無視するようにしています。

HTMLAnchorElement は

.href

でリンク先の URL を取得できるのですが、これは実際に href 属性に設定されている値ではなく、常に絶対 URL で取得できるという特徴があります。

location.origin

で、ページのドメインを取得できるので、これが先頭に含まれるリンクはページ内リンク or 内部リンクとして無視しています。

そして、リンク先の URL をクッションページの URL のうち、 target の値として設定した URL に置換しています。

クッションページ用の JavaScript

クエリパラメータ(target)から元 URL を取得して、その URL に遷移する a タグを生成しています。

XSS 対策として、href 属性に対する URL に含まれる特殊文字をエスケープしていますが、画面に表示するテキストは .textContent に設定しているため、そのままで問題ありません。

別途、「javascript:」から始まる href 属性は JavaScript を実行してしまうため、これを無効化しています。

まとめ

意外と HTML + JavaScript だけでクッションページを作成できることが分かりました。 この記事自体はサーバサイドでプログラムが動いていますが、実際に上記 JavaScript を使用する場合は素の HTML ファイルで問題ないはずです。

もちろん、セキュリティ面などを考慮して、実際に運用する際は十分な検証を行ってください。

プログラマー/N.Go

このメンバーの記事一覧へ

おすすめ記事