Documentation Next.js

はじめに

この記事では、Next.jsアプリケーションにおけるXSS(クロスサイトスクリプティング)対策の実践的な方法を解説します。XSS攻撃は、Webアプリケーションにおける最も一般的な脆弱性の一つであり、適切な対策を講じることが重要です。

本記事で学べること:

  • XSS攻撃の仕組みと種類
  • Content Security Policy(CSP)の設定方法
  • セキュリティヘッダーの導入と設定
  • Next.jsでの実践的な実装パターン

XSS攻撃とは

XSS(クロスサイトスクリプティング)攻撃は、悪意のあるスクリプトをWebページに注入し、ユーザーのブラウザ上で実行させる攻撃手法です。攻撃者は、この手法を使ってCookieの盗難、セッションハイジャック、フィッシング詐欺などを行います。

XSS攻撃の主な種類

種類説明
反射型XSSURLパラメータなどに含まれた悪意のあるスクリプトがそのまま表示される検索クエリの表示
格納型XSSデータベースに保存された悪意のあるスクリプトが表示されるコメント欄への投稿
DOM型XSSクライアントサイドのJavaScriptが不正なデータを処理するURLハッシュの処理

XSS攻撃の危険性

// 危険な例: ユーザー入力をそのまま表示
// 攻撃者が <script>alert('XSS')</script> を入力すると実行される
function DangerousComponent({ userInput }) {
  return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
}

// 安全な例: Reactのデフォルトのエスケープ機能を利用
// HTMLタグは自動的にエスケープされる
function SafeComponent({ userInput }) {
  return <div>{userInput}</div>;
}

Content Security Policy(CSP)の設定

CSPとは

**Content Security Policy(CSP)**は、Webサイトが許可するコンテンツのソースを明示的に指定するセキュリティ機能です。ブラウザに対して「どのソースからのスクリプト、スタイル、画像などを許可するか」を伝えることで、不正なスクリプトの実行を防ぎます。

CSPディレクティブの種類

ディレクティブ説明用途
default-src他のディレクティブが指定されていない場合のデフォルト基本設定
script-srcJavaScriptの読み込み元を制限XSS対策の要
style-srcCSSの読み込み元を制限スタイル注入防止
img-src画像の読み込み元を制限画像スプーフィング防止
connect-srcAPIやWebSocketの接続先を制限データ漏洩防止
frame-srciframeの読み込み元を制限クリックジャッキング防止

Next.jsでのCSP設定

next.config.jsでCSPヘッダーを設定します。

// next.config.js
const { createSecureHeaders } = require("next-secure-headers");

module.exports = {
  async headers() {
    return [
      {
        // すべてのルートに適用
        source: "/(.*)",
        headers: createSecureHeaders({
          contentSecurityPolicy: {
            directives: {
              // デフォルトでは自身のオリジンのみ許可
              defaultSrc: ["'self'"],
              // スクリプトは自身のオリジンとnonce付きのみ許可
              scriptSrc: ["'self'", "'nonce-randomString'"],
              // スタイルは自身のオリジンとインラインスタイルを許可
              styleSrc: ["'self'", "'unsafe-inline'"],
              // 画像は自身のオリジンとdata URIを許可
              imgSrc: ["'self'", "data:", "https:"],
              // APIの接続先を制限
              connectSrc: ["'self'", "https://api.example.com"],
              // フォントの読み込み元を制限
              fontSrc: ["'self'", "https://fonts.gstatic.com"],
              // フレームを完全に禁止
              frameSrc: ["'none'"],
            },
          },
          // リファラーポリシーの設定
          referrerPolicy: "strict-origin-when-cross-origin",
        }),
      },
    ];
  },
};

nonceを使用した動的スクリプトの許可

nonceを使用すると、特定のインラインスクリプトのみを許可できます。

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // リクエストごとにランダムなnonceを生成
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");

  // CSPヘッダーを設定
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
  `
    .replace(/\s{2,}/g, " ")
    .trim();

  const response = NextResponse.next();

  // レスポンスヘッダーにCSPを設定
  response.headers.set("Content-Security-Policy", cspHeader);
  // nonceをリクエストヘッダーに追加(後でコンポーネントで使用)
  response.headers.set("x-nonce", nonce);

  return response;
}

X-XSS-Protectionヘッダーの設定

X-XSS-Protectionとは

X-XSS-Protectionは、ブラウザ内蔵のXSSフィルターを制御するセキュリティヘッダーです。XSS攻撃を検出した場合、ページのレンダリングをブロックできます。

注意: 現代のブラウザではCSPが推奨されており、X-XSS-Protectionは非推奨になりつつあります。しかし、古いブラウザのサポートのために設定しておくことをお勧めします。

設定方法

// next.config.js
const { createSecureHeaders } = require("next-secure-headers");

module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: createSecureHeaders({
          // XSSフィルターを有効化し、検出時はページをブロック
          xssProtection: "1; mode=block",
        }),
      },
    ];
  },
};

X-XSS-Protectionの値

説明
0XSSフィルターを無効化
1XSSフィルターを有効化(検出時はサニタイズ)
1; mode=blockXSSフィルターを有効化(検出時はページをブロック)

X-Content-Type-Optionsヘッダーの設定

X-Content-Type-Optionsとは

X-Content-Type-Optionsは、ブラウザがMIMEタイプを「スニッフィング」(推測)することを防ぐセキュリティヘッダーです。これにより、悪意のあるファイルがスクリプトとして実行されることを防ぎます。

設定方法

// next.config.js
const { createSecureHeaders } = require("next-secure-headers");

module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: createSecureHeaders({
          // MIMEタイプのスニッフィングを禁止
          contentTypeOptions: "nosniff",
        }),
      },
    ];
  },
};

MIMEスニッフィング攻撃の例

// 攻撃者が画像としてアップロードしたファイルが
// 実際にはJavaScriptコードを含んでいる場合
// nosniffがないと、ブラウザがスクリプトとして実行する可能性がある

// サーバー側の対策
app.use((req, res, next) => {
  // Content-Typeを明示的に設定
  res.setHeader("X-Content-Type-Options", "nosniff");
  next();
});

すべてのセキュリティヘッダーを統合した設定

実際のプロジェクトでは、複数のセキュリティヘッダーを組み合わせて設定します。

// next.config.js
const { createSecureHeaders } = require("next-secure-headers");

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: createSecureHeaders({
          // Content Security Policy
          contentSecurityPolicy: {
            directives: {
              defaultSrc: ["'self'"],
              scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
              styleSrc: ["'self'", "'unsafe-inline'"],
              imgSrc: ["'self'", "data:", "https:"],
              connectSrc: ["'self'", "https://api.example.com"],
              fontSrc: ["'self'", "https://fonts.gstatic.com"],
              frameSrc: ["'none'"],
              objectSrc: ["'none'"],
              baseUri: ["'self'"],
              formAction: ["'self'"],
            },
          },
          // XSSフィルター
          xssProtection: "1; mode=block",
          // MIMEスニッフィング防止
          contentTypeOptions: "nosniff",
          // リファラーポリシー
          referrerPolicy: "strict-origin-when-cross-origin",
          // クリックジャッキング防止
          frameGuard: "deny",
          // HTTPSを強制(本番環境のみ)
          forceHTTPSRedirect:
            process.env.NODE_ENV === "production"
              ? [true, { maxAge: 31536000, includeSubDomains: true }]
              : false,
        }),
      },
    ];
  },
};

module.exports = nextConfig;

Next.jsでの追加のXSS対策

ユーザー入力のサニタイズ

// lib/sanitize.ts
import DOMPurify from "isomorphic-dompurify";

/**
 * ユーザー入力をサニタイズする
 * HTMLタグを除去し、安全な文字列に変換
 */
export function sanitizeInput(input: string): string {
  // DOMPurifyを使用して危険なHTMLを除去
  return DOMPurify.sanitize(input, {
    ALLOWED_TAGS: [], // すべてのタグを禁止
    ALLOWED_ATTR: [], // すべての属性を禁止
  });
}

/**
 * 限定的なHTMLを許可してサニタイズする
 * ブログのコメントなど、一部の装飾を許可したい場合
 */
export function sanitizeRichText(input: string): string {
  return DOMPurify.sanitize(input, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "br"],
    ALLOWED_ATTR: ["href", "target"],
  });
}

サニタイズの使用例

// components/CommentForm.tsx
"use client";

import { useState } from "react";
import { sanitizeInput } from "@/lib/sanitize";

export function CommentForm() {
  const [comment, setComment] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    // 送信前にサニタイズ
    const sanitizedComment = sanitizeInput(comment);

    // APIに送信
    await fetch("/api/comments", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ comment: sanitizedComment }),
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder="コメントを入力..."
      />
      <button type="submit">送信</button>
    </form>
  );
}

APIルートでの入力検証

// app/api/comments/route.ts
import { NextResponse } from "next/server";
import { z } from "zod";
import { sanitizeInput } from "@/lib/sanitize";

// 入力のバリデーションスキーマ
const commentSchema = z.object({
  comment: z
    .string()
    .min(1, "コメントは必須です")
    .max(1000, "コメントは1000文字以内で入力してください"),
});

export async function POST(request: Request) {
  try {
    const body = await request.json();

    // バリデーション
    const validatedData = commentSchema.parse(body);

    // サニタイズ
    const sanitizedComment = sanitizeInput(validatedData.comment);

    // データベースに保存
    // await db.comments.create({ data: { content: sanitizedComment } });

    return NextResponse.json({ success: true });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: "入力が不正です", details: error.errors },
        { status: 400 }
      );
    }
    return NextResponse.json(
      { error: "サーバーエラーが発生しました" },
      { status: 500 }
    );
  }
}

まとめ

XSS対策は、Webアプリケーションのセキュリティにおいて非常に重要です。Next.jsでは、以下の対策を組み合わせることで、XSS攻撃のリスクを大幅に軽減できます。

対策効果重要度
CSP(Content Security Policy)スクリプトの実行元を制限
X-XSS-ProtectionブラウザのXSSフィルターを有効化
X-Content-Type-OptionsMIMEスニッフィングを防止
入力サニタイズ悪意のあるコードを除去
入力バリデーション不正な入力を拒否

これらのセキュリティ対策を適切に実装することで、安全なWebアプリケーションを構築しましょう。

参考文献

円