【フロントエンドセキュリティ①】XSSの脅威と防止策の基礎知識

XSS(クロスサイトスクリプティング)は、ウェブアプリケーションにおける代表的な脆弱性の一つであり、これを適切に対策しなければ重大なリスクを引き起こす。この記事では、XSSの概要や攻撃事例、どのようなリスクが伴うかを明確にし、具体的な対策を示す。

XSSとは

XSSは、悪意のあるスクリプトがユーザーのブラウザ上で実行される脆弱性。攻撃者はこの脆弱性を利用し、ユーザーのセッション情報を盗む、悪質なスクリプトを埋め込むなど、様々な攻撃を実行可能。XSSは大きく3つのタイプに分類される。

  • Stored XSS(蓄積型): 悪意のあるスクリプトがサーバーに保存され、複数のユーザーに影響を与えるもの。
  • Reflected XSS(反射型): リクエストの中に含まれるスクリプトがそのまま反映され、即座に実行されるもの。
  • DOM-based XSS: クライアントサイドのDOM操作を悪用し、ユーザーのブラウザ内でスクリプトが実行されるもの。

Stored XSS(蓄積型)

Stored XSSは、最も危険とされるXSS攻撃の一つ。攻撃者は、悪意のあるスクリプトをサーバーに保存させ、それを複数のユーザーが閲覧するページに埋め込み、広範囲に影響を与える。この攻撃は、掲示板やブログのコメント機能、ユーザー生成コンテンツを扱うアプリケーションなどで多く見られる。例えば、攻撃者がコメント欄にスクリプトを投稿し、それを他のユーザーが閲覧した際に、そのスクリプトが実行されるケースが該当する。攻撃者はこの方法で、ユーザーのセッション情報を盗んだり、リダイレクトを利用してフィッシングサイトに誘導することが可能となる。サーバーに一度保存されるため、攻撃の影響が長期的に及ぶことも多い。

Reflected XSS(反射型)

Reflected XSSは、ユーザーのリクエストに含まれるスクリプトがそのままサーバーからレスポンスとして返され、即座にブラウザ上で実行される攻撃。この攻撃は、攻撃者が悪意のあるリンクを作成し、被害者にクリックさせることで成立する。例えば、検索機能やフォームの入力データがそのままページに反映されるようなウェブサイトで、検索クエリにスクリプトを埋め込むことで、スクリプトが実行される。Reflected XSSは、一度のリクエストごとに発生するため、攻撃者はユーザーを特定のリンクやメールなどで誘導する必要がある。攻撃が即時的であり、ターゲットを個別に狙いやすいが、蓄積型のように長期的な影響はない。

DOM-based XSS

DOM-based XSSは、クライアントサイドで実行されるXSS攻撃。サーバー側ではなく、ブラウザがJavaScriptのDOM操作を通じて、ユーザー入力を直接的に処理する際に発生する。この攻撃は、Webページがユーザー入力をDOMに直接反映し、適切なサニタイズ処理を行わなかった場合に生じる。たとえば、URLのパラメータやハッシュに含まれるデータが、そのままJavaScriptによってDOMに挿入されるケースがある。攻撃者はこの脆弱性を利用して、クライアント側でスクリプトを実行し、ユーザーの情報を盗み出すことができる。DOM-based XSSは、サーバーに影響を与えることなくクライアントサイドで完結するため、サーバーログに痕跡が残りにくく、検出が難しいのが特徴。

XSSが引き起こすリスク

XSSがもたらすリスクは多岐にわたる。以下は代表的なもの。

  • セッションハイジャック: 攻撃者がユーザーのセッション情報を取得し、アカウントを乗っ取る可能性がある。
  • フィッシング攻撃: ユーザーを偽のページに誘導し、個人情報を盗む。
  • マルウェア配布: 悪意のあるスクリプトを介して、ユーザーのデバイスにマルウェアをインストールする。

XSS攻撃の実例(DOM-based XSS)

以下のフォームを例に、XSS攻撃がどのように実行されるかを確認する。

攻撃コードの例

このコードでは、ユーザーが入力した内容をoutput.innerHTMLでそのままHTMLとしてページに挿入している。これにより、ユーザーが悪意あるスクリプトを入力した場合、そのスクリプトが実行されることになる。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>XSS攻撃の例</title>
</head>
<body>
    <h1>コメントを入力してください</h1>
    <form id="commentForm">
        <input type="text" id="userInput" placeholder="コメントを入力">
        <button type="submit">送信</button>
    </form>

    <p>入力されたコメント: <span id="output"></span></p>

    <script>
        const form = document.getElementById('commentForm');
        const output = document.getElementById('output');

        form.addEventListener('submit', function (event) {
            event.preventDefault();
            const input = document.getElementById('userInput').value;
            output.innerHTML = input; // ユーザー入力をそのままHTMLとして挿入
        });
    </script>
</body>
</html>

例えば、以下のコードを入力して送信してみると、

<script>alert('XSS Attack!')</script>

script要素として実行されていることがわかる(この時点でアウト)。

この例はあくまでalertの実行だが、セッション情報を盗んで送信されたり、詐欺サイトにリダイレクトされることが多い。

ただし、Chromeなどのモダンブラウザでは、CSP(コンテンツセキュリティポリシー)といって、インラインのスクリプト(<script>タグ内に直接記述されたスクリプト)がデフォルトでブロックされる仕組みがある。ブラウザによっては、意図的に不正なスクリプトの実行を阻止するため、サニタイズされたHTML内の<script>タグを無効にすることがある。

XSSを防ぐための基本的な対策

XSSを防ぐための基本的な対策は、「入力のサニタイズ」と「出力のエスケープ」である。

  • 入力のサニタイズ: ユーザーからの入力は常に信頼できないデータとして扱い、サニタイズを徹底する。特にHTMLやJavaScriptの文脈では危険なタグや属性を削除する必要がある。
  • 出力のエスケープ: HTMLとして出力する際には、特殊文字(<, >, &, “)をエスケープし、スクリプトが実行されないようにする。

入力のサニタイズ処理

以下は、ユーザーの入力をサニタイズする基本的な処理。この処理では、悪意のあるスクリプトを含む可能性のある特殊文字をエスケープして、XSS攻撃を防止する。

function sanitizeInput(input) {
    return input
        .replace(/&/g, "&")   // & を & に変換
        .replace(/</g, "<")    // < を < に変換
        .replace(/>/g, ">")    // > を > に変換
        .replace(/"/g, """)  // " を " に変換
        .replace(/'/g, "'"); // ' を ' に変換
}

// サニタイズの例
const userInput = "<script>alert('XSS Attack!')</script>";
const safeInput = sanitizeInput(userInput);

出力のエスケープ処理

以下は、サニタイズ済みのデータを安全にHTMLに挿入する例。innerHTMLではなく、textContentを使用することで、XSS攻撃を防ぐことができる。

// サニタイズ済みの入力を安全に表示
const outputElement = document.getElementById('output');

// サニタイズされた安全なデータを出力する例
outputElement.textContent = safeInput;

Vue.jsやReact.jsが採用される理由

Vue.jsやReact.jsが多くのプロジェクトで採用される理由の一つに、デフォルトでセキュリティ対策、特にXSS(クロスサイトスクリプティング)攻撃に対する保護が組み込まれている点が挙げられる。

Vue.jsやReact.jsは、HTMLテンプレートにユーザー入力やデータをバインドする際、自動的にデータのエスケープ処理を行う。これにより、開発者が特別な処理を行わなくても、悪意のあるスクリプトがユーザーによって入力されても実行されることなく、安全に表示される。

例: Vue.jsでの自動エスケープ処理

<template>
  <v-container>
    <h1>コメントを入力してください</h1>
    <form id="commentForm">
      <input type="text" v-model="inputValue" placeholder="コメントを入力" />
      <button type="submit">送信</button>
    </form>

    <p>入力されたコメント: {{ inputValue }}</p>
  </v-container>
</template>
<script setup lang="ts">
const inputValue = ref("");
</script>

この例では、inputValueにスクリプトタグを含む入力があっても、Vue.jsが自動的にエスケープ処理を行い、ブラウザ上で<script>タグが実行されることなく、安全に表示される。Vue.jsは、{{ inputValue }}のようにバインドされた値をHTMLとして解釈せず、単純なテキストとして扱うため。

↓実行結果

例: React.jsでの自動エスケープ処理

import { Button, Container, TextField } from "@mui/material";
import { useState } from "react";

export default function Index() {
  const [inputValue, setInputValue] = useState("");
  const changeInput = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = event.target;
    setInputValue(value);
  };
  return (
    <Container maxWidth="sm">
      <h2>コメントを入力してください</h2>
      <form method="post">
        <TextField type="text" name="input" onChange={changeInput} />
        <Button type="submit">送信</Button>
      </form>
      <p>ユーザーの入力: {inputValue}</p>
    </Container>
  );
}

React.jsでも同様に、{inputValue}でバインドされた値はテキストとして扱われ、スクリプトタグを含む入力がHTMLとして実行されることはない(以下の画像を参照: 文字列として出力されている)Reactは、デフォルトでXSS対策を施し、ユーザーからの入力を安全にレンダリングする。

↓実行結果

※【注意】dangerouslySetInnerHTMLv-html

React.jsのdangerouslySetInnerHTMLやVue.jsのv-htmlは、直接HTMLをレンダリングする = XSSのリスクが高まるため、慎重に扱う必要がある。コンテンツをDOMに表示させるときに、HTMLとして描画したいケースはあるが、XSSのリスクを踏まえて検討すべきである。

まとめ

XSS(クロスサイトスクリプティング)は、ウェブアプリケーションに深刻なリスクをもたらす脆弱性の一つ。Stored XSS、Reflected XSS、DOM-based XSSといった3つの攻撃タイプに分類され、それぞれがユーザーのセッション情報を盗む、マルウェアを配布する、フィッシング攻撃を行うなど、さまざまな脅威をもたらす。これを防ぐためには、入力データをサニタイズし、出力時にエスケープ処理を徹底することが重要となる。

Vue.jsやReact.jsなどのモダンなフロントエンドフレームワークは、デフォルトでXSS対策を組み込んでおり、ユーザー入力を安全に処理する機能が備わっている。ただし、危険なメソッドであるdangerouslySetInnerHTMLv-htmlの使用には十分な注意が必要だ。

関連記事

コメント

この記事へのトラックバックはありません。