はじめに
この記事では、Next.jsアプリケーションにおけるXSS(クロスサイトスクリプティング)対策の実践的な方法を解説します。XSS攻撃は、Webアプリケーションにおける最も一般的な脆弱性の一つであり、適切な対策を講じることが重要です。
本記事で学べること:
- XSS攻撃の仕組みと種類
- Content Security Policy(CSP)の設定方法
- セキュリティヘッダーの導入と設定
- Next.jsでの実践的な実装パターン
XSS攻撃とは
XSS(クロスサイトスクリプティング)攻撃は、悪意のあるスクリプトをWebページに注入し、ユーザーのブラウザ上で実行させる攻撃手法です。攻撃者は、この手法を使ってCookieの盗難、セッションハイジャック、フィッシング詐欺などを行います。
XSS攻撃の主な種類
| 種類 | 説明 | 例 |
|---|---|---|
| 反射型XSS | URLパラメータなどに含まれた悪意のあるスクリプトがそのまま表示される | 検索クエリの表示 |
| 格納型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-src | JavaScriptの読み込み元を制限 | XSS対策の要 |
style-src | CSSの読み込み元を制限 | スタイル注入防止 |
img-src | 画像の読み込み元を制限 | 画像スプーフィング防止 |
connect-src | APIやWebSocketの接続先を制限 | データ漏洩防止 |
frame-src | iframeの読み込み元を制限 | クリックジャッキング防止 |
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の値
| 値 | 説明 |
|---|---|
0 | XSSフィルターを無効化 |
1 | XSSフィルターを有効化(検出時はサニタイズ) |
1; mode=block | XSSフィルターを有効化(検出時はページをブロック) |
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-Options | MIMEスニッフィングを防止 | 中 |
| 入力サニタイズ | 悪意のあるコードを除去 | 高 |
| 入力バリデーション | 不正な入力を拒否 | 高 |
これらのセキュリティ対策を適切に実装することで、安全なWebアプリケーションを構築しましょう。