クロスサイトスクリプティング (XSS) とは?

Web 上のセキュリティ確保は、終わりのない戦いです。サーバーの立ち上げは数分で終わりますが、次の瞬間には、既にハッキングが仕掛けられてしまうのです。そのような攻撃は、悪意のあるボットによる自動化のものもあれば、手作業で行われていることもあります。Web サイトは、Web プレゼンスやデータを奪おうとする悪意のあるユーザーから狙われることがあります。そうした攻撃手法の 1 つがクロスサイト スクリプティング (XSS) です。皆さんのサイトも XSS 脆弱性を抱えているかもしれません。

XSS (クロスサイトスクリプティング) とは、アプリケーションのデータ入力箇所の抜け穴を悪用した攻撃です。フォーム フィールドやアドレス バーにスクリプトを埋め込んだコードを挿入してアプリケーションに送り、悪意のあるコードを実行させます。その結果、Cookie などの機密情報が漏えいしたり、Web ページ上でスクリプトが実行されページに異質な要素が挿入されたりする被害が発生します。

このチュートリアルでは、ブラウザー テストによりこのような XSS 攻撃を防止する方法について説明します。

前提条件

このチュートリアルを進めるには、いくつかの準備が必要です。

  1. JavaScript の基礎を理解する
  2. Node.js (バージョン 11 以上) をローカル システムにインストールする
  3. CircleCI アカウントを用意する
  4. GitHub アカウントを用意する

サンプル アプリケーションのクローンと実行

まず、XSS 攻撃のテスト対象となるデモ アプリケーションをクローンする必要があります。以下のコマンドを実行して、コードをローカル システムに取得します。

git clone --single-branch --branch base-project https://github.com/coderonfleek/xss-attacks.git

アプリケーションをクローンしたら、プロジェクトのルートに移動 (cd xss-attacks) し、以下のコマンドを実行して依存関係をインストールします。

npm install

依存関係がすべてインストールされたら、以下のコマンドでアプリケーションを実行します。

node server

これで、http://localhost:5000 でアプリケーション サーバーが立ち上がります。ブラウザーでこの URL にアクセスします。

Homepage - Demo App

このページはフォームと、右側に情報を表示する列で構成されています。フォームにメール アドレスを入力して Enter キーを押すと、そのアドレスが [Details (詳細)] ボックスに表示されます。

Fill form - デモ アプリ

XSS 攻撃の手動テスト

フォームを送信すると、その情報がバックエンドのサーバーのエンドポイント (/sendinfo) に送信されます。 このエンドポイントは、メール アドレスを json 応答の本文として送信します。この json 応答はその後ページで取得され、[Details (詳細)] セクションに表示されます。つまり、表示されるメール アドレスは、フォームに入力された後、バックエンドに送られ、ページに戻されるプロセスを経たデータなのです。悪意のあるユーザーなら、不正なデータをメール フィールドに入力するだけで、このプロセスを簡単に悪用できてしまいます。たとえば、メール フィールドに有効なメール アドレスではなく、ファイル フィールドを表示する HTML マークアップを入力してみましょう。

<input type="file" />

パスワード フィールドに入力して[Submit(送信)] をクリックします。すると、[Details (詳細)] セクションの表示はまったく異なったものになります。

XSSに攻撃されたページ - デモアプリ

入力したデータによって、新しい HTML 要素 (ファイル入力フィールド) がページに表示されました。これは明らかに望ましくない挙動で、工夫次第では危険をもたらしかねません。たとえば、攻撃者はこの手法を使用して、悪意のあるデータ (またはスクリプト) をフォームに埋め込みます。フォーム内に非表示の入力フィールドを配置すれば、フォームからペイロードと一緒に危険なデータをサーバーに送ることができます。その結果、深刻な被害が発生し、データの完全性が損なわれるおそれがあります。このような脆弱性は、悪用される前に特定し、修正しなければなりません。その効率的な方法の 1 つがブラウザー テストです。

Jest と Puppeteer のインストール

ブラウザー テストでは、普通のユーザーが行うように Web ページを操作してページのテストを行います。さまざまなデータ入力シナリオをテストすることで、ハッカーに狙われる脆弱性を見つけ、修正できます。

自動ブラウザー テストをセットアップするには、次の 2 つのパッケージが必要です。

  • Jest: テスト スイートのテストランナーとして使用します。
  • Puppeteer: ブラウザー テストの作成に使用します。

以下のコマンドを実行して、これらのパッケージをインストールします。

npm install --save-dev jest puppeteer

これらのパッケージのインストールが完了したら、ブラウザー テストの作成に取り掛かりましょう。

ブラウザー テストの追加

このセクションでは、ブラウザーをテストしメール フィールドの脆弱性を検出するためのテスト スイートを作成します。脆弱性が発見されると、テストは失敗します。テストが失敗した場合、メール フィールドに抜け穴があり、XSS 攻撃を防ぐための対処が必要だということになります。

これから作成するテストでは、前のセクションにて手動で行ったのと同じ攻撃を実行します。テスト ファイル login.test.js を作成し、以下のコードを入力します。

const puppeteer = require("puppeteer");

test("Check for XSS attack on email field", async () => {
  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();

    await page.goto("http://localhost:5000");

    await page.type("#userEmail", '<input type="file" />');
    await page.type("#userPassword", "password");
    await page.click("#submitButton");

    let emailContainer = await page.$("#infoDisplay");
    let value = await emailContainer.evaluate((el) => el.textContent);

    expect(value.length).toBeGreaterThan(0);
  } finally {
    await browser.close();
  }
}, 120000);

上記のテスト ファイルで作成した Check for XSS attack on email field テスト ケースの内容は次のとおりです。まず、Puppeteer を使用してブラウザー インスタンスを起動し、URL http://localhost:5000 でアプリケーションを読み込みます。アプリケーションが実行されたら、メール フィールドにファイル入力マークアップを入力します。次に、パスワード フィールドに入力します。 その後、Submit (送信)] ボタンをクリックし、フォームを送信します。そして、表示セクションの文字列の長さが 1 以上かどうかチェックします (非テキストの HTML 要素は長さが 0 の文字列を返します)。最後に、テストの実行が完了したら、ブラウザーを閉じます。

では、攻撃の有無をチェックするために、テストを実装しましょう。テストをセットアップするため、以下のように test スクリプトを package.json ファイルに追加します。

...
"scripts" : {
    "test" : "jest"
}

アプリケーションが node server.js を使用して実行されていることを確認してから、テスト ファイルを実行します。

npm run test

既にご存じのとおり、脆弱性が存在するため、このテストは失敗します。CLI 出力には、以下のように表示されます。

 FAIL  ./login.test.js (5.475 s)
  ✕ Check for XSS attack on email field (2096 ms)

  ● Check for XSS attack on email field

    expect(received).toBeGreaterThan(expected)

    Expected: > 0
    Received:   0

      23 |   let value = await emailContainer.evaluate(el => el.textContent);
      24 |
    > 25 |   expect(value.length).toBeGreaterThan(0);
         |                        ^
      26 |
      27 | }, 120000);
      28 |

      at Object.<anonymous> (login.test.js:25:24)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        14.582 s
Ran all test suites.

XSS 脆弱性の修正

XSS 脆弱性に対する効果的な解決策の 1 つは、アプリケーションに入力されたデータを、そのデータの処理前に検証することです。データの検証は、アプリケーションのクライアント側とサーバー側の両方で実施できます。今回のアプリケーションでは、受け取ったメール アドレスをサーバー側で検証し、安全なテキストだけをクライアントに返すようにしましょう。

まず、server.js を開きます。 現在、/sendinfo エンドポイントは、検証なしでメール アドレスをクライアントに返すようになっています。

app.post("/sendinfo", (req, res) => {
  const email = req.body.email;

  res.send({ email });
});

このエンドポイントを以下のコードに置き替えます。

app.post("/sendinfo", (req, res) => {
  let email = req.body.email;

  if (!validEmail(email)) {
    email = "Enter a Valid Email e.g test@company.com";
  }

  res.send({ email });
});

function validEmail(mail) {
  return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(
    mail
  );
}

この新しいコードの validEmail 関数は、文字列を受け取り、その文字列が有効なメール アドレスかどうかに応じてブール値を返します。この関数は /sendinfo で使用され、クライアントから送信されたメール アドレスを検証します。メール アドレスが有効な場合は、そのメール アドレスをクライアントに返します。無効な場合は、ユーザーに有効なメール アドレスの入力を求めるメッセージを送信します。

server.js コードの変更が完了したら、アプリケーションを強制終了(Ctrl + C) してから、再起動 (node server.js)します。まずは、手動でブラウザーを更新して攻撃を試み、テストを行いましょう。今回は、入力フィールドではなく検証メッセージが表示されます。

成功したてマニュアルテストの例 - デモアプリ

次に、コマンド npm run test でテスト スイートを実行します。テストは成功し、コンソール出力に結果が示されます。

 PASS  ./login.test.js (6.953 s)
  ✓ Check for XSS attack on email field (3887 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        16.258 s
Ran all test suites.

テスト プロセスの自動化

このチュートリアルの大きな目的は、本番環境のコードに XSS 脆弱性が入り込むことのないように、ブラウザー テストのプロセスを自動化することにあります。

自動化を開始するにはまず、コードを GitHub にプッシュする必要があります。

次に、CircleCI ダッシュボードの [Projects(プロジェクト)] ページに移動して、プロジェクトを追加します。

プロジェクトの追加 - CircleCI

[Set Up Project (プロジェクトをセットアップ)] をクリックして、プロジェクトのセットアップを開始します。 表示されるモーダルで [Skip this step (このステップをスキップ)] をクリックします。CircleCI 設定ファイルは、このチュートリアル後半にて、手動で追加します。

コンフィグの追加 - CircleCI

セットアップ ページで、表示されたサンプルを無視して設定ファイルを手動で追加するため、[Use Existing Config (既存の設定ファイルを使用する)] をクリックします。パイプラインの設定ファイルをダウンロードするのか、ビルドを開始するのかを確認するメッセージが表示されます。

Build Prompt - CircleCI

[Start Building (ビルドの開始)] をクリックして開始します。設定ファイルのセットアップがまだのため、このビルドは失敗します。このセットアップ作業はこの後行います。

最後に、ブラウザー テストを実行して自動デプロイ パイプラインを作成するための CircleCI のデプロイ スクリプトを作成します。プロジェクトのルートに、.circleci という名前のフォルダーを作成して、config.yml という名前のファイルをその中に作成します config.yml に、以下のコードを入力します。

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: circleci/node:12-browsers
    steps:
      - checkout
      - run:
          name: NPM のアップデート
          command: "sudo npm install -g npm"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: 依存関係のインストール
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: アプリケーションの実行
          command: node server.js
          background: true
      - run:
          name: テストの実行
          command: npm run test

このスクリプトはまず、必要なイメージをプルして npm を更新します。その後、依存関係をインストールし、キャッシュに保存します。ブラウザー テストが確実に実行されるように、アプリケーションをバックグラウンド プロセスで開始します。アプリケーションが起動したら test スクリプトを実行してアプリケーションをテストします。

すべての変更をプロジェクトにコミットして、リモート GitHub リポジトリにプッシュします。これで自動的にビルド パイプラインがトリガーされ、ビルドが成功します。

ビルド成功例 - CircleCI

[build(ビルド)] をクリックして、テストの詳細を確認します。

ビルドの内容 - CircleCI

まとめ

ビルド プロセスにセキュリティ チェックを組み込むと、コードの価値を大きく高めることができます。コードはバグなく機能するだけでは不十分です。悪用されないようにセキュリティの確保もしなければなりません。ここで紹介したようなセキュリティ主導型の開発プロセスを、ユーザーからの操作を受けつける他のコード部分にも拡張すれば、悪意のあるユーザーによるコードの脆弱性の悪用を防ぐことができます。コードに対する XSS 攻撃を阻止するため、本記事の手順を実施するようチーム メンバーに促しましょう。

すばらしいコーディングができますように!


Fikayo Adepoju は、Web とモバイル テクノロジー、DevOps に精通した LinkedIn Learning (Lynda.com) 講師、フルスタック開発者、テクニカル ライター、テクニカル コンテンツ クリエイターです。スケーラブルな分散アプリケーション開発については 10 年以上の経験を持っています。CircleCI、Twilio、Auth0、The New Stack のブログで 40 以上の記事を執筆するほか、個人の Medium ページでも情報を発信しており、役立つ知識を多くの開発者に広めることに専心しています。また、Udemyで動画形式のコースも開講しています。ぜひご覧ください。