T-CREATOR

Nuxt 本番運用チェックリスト:セキュリティヘッダー・CSP・Cookie 設定を総点検

Nuxt 本番運用チェックリスト:セキュリティヘッダー・CSP・Cookie 設定を総点検

Nuxt.js でアプリケーションを本番環境にデプロイする際、機能の実装だけでは不十分です。セキュリティヘッダーや Content Security Policy(CSP)、Cookie の適切な設定が欠けていると、XSS やクリックジャッキング、セッションハイジャックなどの攻撃にさらされてしまいます。

本記事では、Nuxt アプリケーションを安全に運用するために必須のセキュリティ設定を、チェックリスト形式で網羅的にご紹介します。設定方法だけでなく、各項目の意味や効果も丁寧に解説しますので、初心者の方でも安心して実装できるでしょう。

背景

近年、Web アプリケーションへのサイバー攻撃は巧妙化しており、開発者は機能実装と並行してセキュリティ対策にも注力する必要があります。特に、ブラウザとサーバー間の通信を保護するセキュリティヘッダーは、攻撃者による悪意あるスクリプト実行やデータ窃取を防ぐ最前線の防御壁となります。

Nuxt.js はフルスタックフレームワークとして、サーバーサイドレンダリング(SSR)やハイブリッドレンダリングをサポートしているため、セキュリティヘッダーの設定がより重要になります。なぜなら、SSR ではサーバー側で HTML を生成してクライアントに送信するため、サーバーレスポンスに適切なヘッダーを付与しなければ、ブラウザ側でのセキュリティ機能が働かないからです。

以下の図は、Nuxt アプリケーションにおけるセキュリティヘッダーの役割を示しています。

mermaidflowchart LR
  user["読者<br/>ブラウザ"] -->|HTTPS リクエスト| nuxt["Nuxt アプリ<br/>(SSR/SSG)"]
  nuxt -->|HTML + セキュリティヘッダー| user
  user -->|ヘッダー読み込み| browser["ブラウザ<br/>セキュリティ機能"]
  browser -->|XSS ブロック<br/>クリックジャック防御| user

図のポイント: セキュリティヘッダーはサーバーからブラウザへ送信され、ブラウザ側でセキュリティポリシーを適用します。これにより、悪意ある外部スクリプトの実行やフレームへの埋め込みを防ぎます。

課題

Nuxt アプリケーションを本番環境へデプロイする際、以下のような課題に直面することが多いでしょう。

セキュリティヘッダーの欠如

デフォルトの Nuxt 設定では、セキュリティヘッダーが自動的に付与されません。そのため、開発者が明示的に設定しなければ、以下のようなリスクにさらされます。

  • XSS(クロスサイトスクリプティング)攻撃: 悪意あるスクリプトがページ内で実行される
  • クリックジャッキング: iframe に埋め込まれ、ユーザーが意図しない操作を強制される
  • MIME タイプスニッフィング: ブラウザが意図しない形式でファイルを解釈し、スクリプトを実行してしまう

Content Security Policy(CSP)の複雑さ

CSP は強力なセキュリティ機能ですが、設定が複雑で、誤った設定をすると正常な機能が動作しなくなることがあります。特に、インラインスクリプトや外部リソースの読み込みを制限するため、慎重な設定が必要です。

認証トークンやセッション情報を Cookie に保存する場合、HttpOnlySecureSameSite 属性を適切に設定しないと、以下のリスクがあります。

  • セッションハイジャック: JavaScript から Cookie が読み取られ、攻撃者がセッションを乗っ取る
  • CSRF(クロスサイトリクエストフォージェリ)攻撃: 外部サイトから不正なリクエストが送信される

以下の図は、セキュリティ設定が不足している場合の攻撃フローを示します。

mermaidflowchart TD
  attacker["攻撃者"] -->|悪意あるスクリプト注入| vulnerable["脆弱な Nuxt アプリ"]
  vulnerable -->|セキュリティヘッダーなし| browser["読者のブラウザ"]
  browser -->|スクリプト実行| xss["XSS 攻撃成功"]
  browser -->|Cookie 読み取り| hijack["セッション<br/>ハイジャック"]
  browser -->|iframe 埋め込み| clickjack["クリック<br/>ジャッキング"]

図のポイント: セキュリティヘッダーが設定されていない場合、攻撃者は複数の手法でアプリケーションを攻撃できます。これを防ぐには、ヘッダーを適切に設定する必要があります。

解決策

Nuxt アプリケーションのセキュリティを強化するには、以下の 3 つの主要な設定を行います。

  1. セキュリティヘッダーの設定: nuxt.config.ts でレスポンスヘッダーを追加
  2. Content Security Policy(CSP)の導入: 外部リソースやスクリプト実行を制限
  3. Cookie のセキュリティ属性設定: HttpOnlySecureSameSite を適用

これらの設定を段階的に実装していきましょう。

セキュリティヘッダーの設定方法

Nuxt では、nuxt.config.tsrouteRulesnitro.routeRules を使用してセキュリティヘッダーを追加できます。以下は、本番環境で必須のヘッダー設定です。

nuxt.config.ts の基本構成

まず、nuxt.config.ts ファイルを開き、以下のように nitro セクションを追加します。

typescript// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/**': {
        headers: {
          // 以下でセキュリティヘッダーを設定
        },
      },
    },
  },
});

コード解説: nitro.routeRules を使用すると、すべてのルート(​/​**)に対して共通のヘッダーを設定できます。これにより、個別にミドルウェアを作成する手間が省けます。

X-Content-Type-Options ヘッダー

このヘッダーは、ブラウザが MIME タイプを推測して解釈することを防ぎます。

typescriptheaders: {
  'X-Content-Type-Options': 'nosniff'
}

効果: ブラウザがファイルの Content-Type を勝手に変更せず、サーバーが指定した MIME タイプのみを使用します。これにより、攻撃者が画像ファイルにスクリプトを埋め込んで実行させる攻撃を防げます。

X-Frame-Options ヘッダー

このヘッダーは、ページが iframe 内に埋め込まれることを防ぎます。

typescriptheaders: {
  'X-Frame-Options': 'DENY'
}

効果: クリックジャッキング攻撃を防止します。DENY を設定すると、どのサイトからも iframe 内に埋め込むことができなくなります。同一オリジンのみ許可する場合は SAMEORIGIN を使用します。

X-XSS-Protection ヘッダー

このヘッダーは、ブラウザの XSS フィルターを有効にします(ただし、CSP が優先されます)。

typescriptheaders: {
  'X-XSS-Protection': '1; mode=block'
}

効果: 古いブラウザでも XSS 攻撃を検知してページの読み込みをブロックします。ただし、モダンブラウザでは CSP が推奨されます。

Referrer-Policy ヘッダー

このヘッダーは、リンクをクリックした際に送信される Referer 情報を制御します。

typescriptheaders: {
  'Referrer-Policy': 'strict-origin-when-cross-origin'
}

効果: 同一オリジン内では完全な URL を送信し、外部サイトへはオリジンのみを送信します。これにより、プライバシーを保護しつつ、アクセス解析も可能です。

Strict-Transport-Security ヘッダー

このヘッダーは、ブラウザに HTTPS のみでアクセスするよう強制します。

typescriptheaders: {
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
}

効果: HTTP から HTTPS へのリダイレクト前に攻撃者が介入する「中間者攻撃」を防ぎます。max-age は秒数で、1 年間(31536000 秒)HTTPS を強制します。

Permissions-Policy ヘッダー

このヘッダーは、ブラウザの機能(カメラ、マイク、位置情報など)の使用を制限します。

typescriptheaders: {
  'Permissions-Policy': 'camera=(), microphone=(), geolocation=()'
}

効果: 不要な機能へのアクセスを制限し、プライバシーを保護します。必要な機能のみを許可することで、攻撃者による悪用を防げます。

Content Security Policy(CSP)の設定

CSP は、読み込み可能なリソースのオリジンやスクリプトの実行方法を制限する強力なセキュリティ機能です。Nuxt では、CSP を nuxt.config.ts で設定できます。

CSP の基本設定

以下は、CSP の基本的な設定例です。

typescript// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/**': {
        headers: {
          'Content-Security-Policy': [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
            "style-src 'self' 'unsafe-inline'",
            "img-src 'self' data: https:",
            "font-src 'self' data:",
            "connect-src 'self'",
            "frame-ancestors 'none'",
          ].join('; '),
        },
      },
    },
  },
});

コード解説: CSP ディレクティブを配列で定義し、join('; ') で結合しています。これにより、可読性が向上します。

各ディレクティブの意味

CSP のディレクティブは多岐にわたりますので、主要なものを解説します。

default-src 'self'

typescript"default-src 'self'";

すべてのリソースを同一オリジンからのみ読み込むことを許可します。これがデフォルトのポリシーとなります。

script-src 'self' 'unsafe-inline' 'unsafe-eval'

typescript"script-src 'self' 'unsafe-inline' 'unsafe-eval'";

JavaScript の読み込みと実行を制御します。'self' は同一オリジン、'unsafe-inline' はインラインスクリプト、'unsafe-eval'eval() の使用を許可します。

注意: 'unsafe-inline''unsafe-eval' はセキュリティリスクがあるため、可能な限り避けるべきです。代わりに、nonce や hash を使用することを推奨します。

style-src 'self' 'unsafe-inline'

typescript"style-src 'self' 'unsafe-inline'";

CSS の読み込みを制御します。'unsafe-inline' はインライン CSS を許可しますが、セキュリティリスクがあります。

img-src 'self' data: https:

typescript"img-src 'self' data: https:";

画像の読み込み元を制限します。data: スキームと HTTPS の外部サイトからの読み込みを許可します。

font-src 'self' data:

typescript"font-src 'self' data:";

フォントの読み込み元を制限します。data: スキームを許可することで、Base64 エンコードされたフォントも使用できます。

connect-src 'self'

typescript"connect-src 'self'";

fetchXMLHttpRequest、WebSocket などの接続先を制限します。API エンドポイントが外部ドメインの場合は、そのドメインを追加します。

frame-ancestors 'none'

typescript"frame-ancestors 'none'";

ページが iframe 内に埋め込まれることを防ぎます。X-Frame-Options の CSP 版です。

CSP の段階的導入(Report-Only モード)

CSP を一度に適用すると、既存の機能が動作しなくなる可能性があります。そこで、まずは Content-Security-Policy-Report-Only ヘッダーを使用して、違反をレポートするだけのモードで運用することをおすすめします。

typescriptheaders: {
  'Content-Security-Policy-Report-Only': [
    "default-src 'self'",
    "report-uri /api/csp-report"
  ].join('; ')
}

効果: CSP 違反が発生した場合、​/​api​/​csp-report エンドポイントにレポートが送信されます。これにより、本番環境で問題が発生する前に CSP の調整ができます。

Cookie を使用して認証トークンやセッション情報を保存する場合、以下の属性を設定する必要があります。

HttpOnly 属性

この属性を設定すると、JavaScript から Cookie にアクセスできなくなります。

typescript// サーバーサイドで Cookie を設定する例(Nuxt API ルート)
export default defineEventHandler((event) => {
  setCookie(event, 'session', 'abc123', {
    httpOnly: true,
  });
});

効果: XSS 攻撃によって Cookie が盗まれることを防ぎます。

Secure 属性

この属性を設定すると、Cookie は HTTPS 接続でのみ送信されます。

typescriptsetCookie(event, 'session', 'abc123', {
  httpOnly: true,
  secure: true,
});

効果: HTTP 接続で Cookie が傍受されることを防ぎます。本番環境では必須の設定です。

SameSite 属性

この属性を設定すると、クロスサイトリクエストでの Cookie 送信を制限できます。

typescriptsetCookie(event, 'session', 'abc123', {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
});

効果: CSRF 攻撃を防ぎます。strict は最も厳格な設定で、すべてのクロスサイトリクエストで Cookie を送信しません。lax はトップレベルナビゲーション(リンククリック)では送信します。

以下は、セキュアな Cookie 設定の完全な例です。

typescript// server/api/login.post.ts
export default defineEventHandler((event) => {
  // 認証処理(省略)

  setCookie(event, 'session', 'abc123', {
    httpOnly: true, // JavaScript からアクセス不可
    secure: true, // HTTPS のみ
    sameSite: 'strict', // クロスサイトリクエストで送信しない
    maxAge: 60 * 60 * 24 * 7, // 7 日間有効
    path: '/', // すべてのパスで有効
  });

  return { success: true };
});

コード解説: この設定により、セッション Cookie は XSS、中間者攻撃、CSRF から保護されます。maxAge は秒数で指定し、7 日間(604800 秒)有効にしています。

以下の図は、セキュリティ設定が適用された場合の防御フローを示します。

mermaidflowchart TD
  attacker["攻撃者"] -->|悪意あるスクリプト注入| secure["セキュアな Nuxt アプリ"]
  secure -->|CSP ヘッダー| browser["読者のブラウザ"]
  browser -->|スクリプトブロック| csp_block["CSP により<br/>実行拒否"]
  secure -->|HttpOnly Cookie| browser
  browser -->|JavaScript アクセス拒否| cookie_block["Cookie 読み取り<br/>不可"]
  secure -->|X-Frame-Options| browser
  browser -->|iframe 拒否| frame_block["埋め込み<br/>ブロック"]

図のポイント: セキュリティヘッダーと Cookie 属性が適切に設定されていれば、ブラウザが自動的に攻撃をブロックします。

具体例

ここでは、Nuxt アプリケーションに完全なセキュリティ設定を適用する具体例をご紹介します。

完全な nuxt.config.ts の設定

以下は、本番環境で使用できる完全な nuxt.config.ts の例です。

typescript// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/**': {
        headers: {
          // MIME タイプスニッフィング防止
          'X-Content-Type-Options': 'nosniff',

          // クリックジャッキング防止
          'X-Frame-Options': 'DENY',

          // XSS フィルター有効化
          'X-XSS-Protection': '1; mode=block',

          // Referer 情報の制御
          'Referrer-Policy':
            'strict-origin-when-cross-origin',

          // HTTPS 強制
          'Strict-Transport-Security':
            'max-age=31536000; includeSubDomains; preload',

          // ブラウザ機能の制限
          'Permissions-Policy':
            'camera=(), microphone=(), geolocation=()',

          // Content Security Policy
          'Content-Security-Policy': [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com",
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
            "img-src 'self' data: https:",
            "font-src 'self' data: https://fonts.gstatic.com",
            "connect-src 'self' https://api.example.com",
            "frame-ancestors 'none'",
            "base-uri 'self'",
            "form-action 'self'",
          ].join('; '),
        },
      },
    },
  },
});

コード解説: この設定では、すべての主要なセキュリティヘッダーを網羅しています。script-srcstyle-src には外部 CDN を許可していますが、本番環境では必要最小限に絞ることが重要です。

次に、認証 API でのセキュアな Cookie 設定例を見てみましょう。

typescript// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  // リクエストボディから認証情報を取得
  const body = await readBody(event);
  const { email, password } = body;

  // 認証処理(実際にはデータベースで検証)
  if (
    email === 'user@example.com' &&
    password === 'password'
  ) {
    // セッショントークンを生成
    const sessionToken = generateSecureToken();

    // セキュアな Cookie を設定
    setCookie(event, 'session', sessionToken, {
      httpOnly: true, // JavaScript からアクセス不可
      secure: true, // HTTPS のみ
      sameSite: 'strict', // CSRF 防止
      maxAge: 60 * 60 * 24 * 7, // 7 日間有効
      path: '/', // すべてのパスで有効
    });

    return {
      success: true,
      message: 'ログインに成功しました',
    };
  }

  throw createError({
    statusCode: 401,
    message: '認証に失敗しました',
  });
});

コード解説: setCookie 関数で Cookie を設定する際、すべてのセキュリティ属性を指定しています。これにより、セッショントークンが安全に保存されます。

セキュアトークンの生成関数

Cookie に保存するトークンは、予測不可能でランダムな値である必要があります。

typescript// server/utils/generateSecureToken.ts
import { randomBytes } from 'crypto';

export function generateSecureToken(): string {
  // 32 バイトのランダムなバイト列を生成し、16 進数文字列に変換
  return randomBytes(32).toString('hex');
}

効果: crypto.randomBytes を使用することで、暗号学的に安全なランダムトークンを生成できます。これにより、攻撃者がトークンを推測することは実質的に不可能になります。

ミドルウェアでのセキュリティチェック

サーバーミドルウェアを使用して、リクエストごとにセキュリティチェックを行うこともできます。

typescript// server/middleware/security.ts
export default defineEventHandler((event) => {
  // HTTPS 接続のチェック(本番環境のみ)
  if (process.env.NODE_ENV === 'production') {
    const protocol =
      event.node.req.headers['x-forwarded-proto'];
    if (protocol !== 'https') {
      throw createError({
        statusCode: 403,
        message: 'HTTPS 接続が必要です',
      });
    }
  }

  // Origin ヘッダーの検証
  const origin = event.node.req.headers.origin;
  const allowedOrigins = [
    'https://example.com',
    'https://www.example.com',
  ];

  if (origin && !allowedOrigins.includes(origin)) {
    throw createError({
      statusCode: 403,
      message:
        '許可されていないオリジンからのリクエストです',
    });
  }
});

コード解説: このミドルウェアは、すべてのリクエストに対して HTTPS 接続と Origin ヘッダーを検証します。本番環境では必ず HTTPS を使用し、許可されたオリジンからのリクエストのみを受け付けます。

CSP 違反レポートの受信

CSP 違反が発生した場合、サーバーでレポートを受信して記録できます。

typescript// server/api/csp-report.post.ts
export default defineEventHandler(async (event) => {
  // CSP 違反レポートを取得
  const report = await readBody(event);

  // ログに記録(実際にはデータベースやログサービスに送信)
  console.error(
    'CSP 違反が発生しました:',
    JSON.stringify(report, null, 2)
  );

  // ステータス 204(No Content)を返す
  setResponseStatus(event, 204);
  return null;
});

コード解説: CSP 違反レポートは JSON 形式で送信されます。このエンドポイントでレポートを受信し、ログに記録することで、CSP の調整に役立てることができます。

環境別の設定管理

開発環境と本番環境でセキュリティ設定を切り替える方法もご紹介します。

typescript// nuxt.config.ts
const isProduction = process.env.NODE_ENV === 'production';

export default defineNuxtConfig({
  nitro: {
    routeRules: {
      '/**': {
        headers: {
          'X-Content-Type-Options': 'nosniff',
          'X-Frame-Options': 'DENY',
          'Referrer-Policy':
            'strict-origin-when-cross-origin',

          // 本番環境のみ HSTS を有効化
          ...(isProduction && {
            'Strict-Transport-Security':
              'max-age=31536000; includeSubDomains; preload',
          }),

          // CSP も環境別に設定
          'Content-Security-Policy': isProduction
            ? [
                "default-src 'self'",
                "script-src 'self'",
                "style-src 'self'",
                "img-src 'self' data: https:",
                "font-src 'self' data:",
                "connect-src 'self'",
                "frame-ancestors 'none'",
              ].join('; ')
            : [
                "default-src 'self'",
                "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
                "style-src 'self' 'unsafe-inline'",
              ].join('; '),
        },
      },
    },
  },
});

コード解説: process.env.NODE_ENV を使用して、本番環境と開発環境でヘッダーを切り替えています。開発環境では CSP を緩和し、本番環境では厳格な設定を適用します。

以下の表は、環境別のセキュリティ設定の違いをまとめたものです。

#ヘッダー開発環境本番環境
1X-Content-Type-Optionsnosniffnosniff
2X-Frame-OptionsDENYDENY
3Strict-Transport-Security未設定max-age=31536000
4CSP script-src'unsafe-inline' 'unsafe-eval' 許可'self' のみ
5CSP style-src'unsafe-inline' 許可'self' のみ

図で理解できる要点:

  • 開発環境では CSP を緩和し、開発効率を優先
  • 本番環境では厳格な CSP を適用し、セキュリティを最優先
  • HSTS は本番環境のみで有効化(開発環境では HTTP を使用する場合があるため)

セキュリティヘッダーの検証方法

設定が正しく適用されているか確認するには、ブラウザの開発者ツールを使用します。

以下は、Chrome DevTools でのヘッダー確認手順です。

  1. アプリケーションにアクセス
  2. DevTools を開く(F12 キー)
  3. Network タブを選択
  4. ページをリロード
  5. 最初のリクエスト(通常は HTML ドキュメント)をクリック
  6. Headers セクションの Response Headers を確認

設定したセキュリティヘッダーが表示されていれば、正しく適用されています。

以下の図は、セキュリティヘッダー検証のフローを示します。

mermaidflowchart LR
  dev["開発者"] -->|アクセス| app["Nuxt アプリ"]
  app -->|レスポンス + ヘッダー| browser["ブラウザ"]
  browser -->|DevTools 起動| inspect["Network タブ<br/>確認"]
  inspect -->|ヘッダー確認| verify["セキュリティ<br/>ヘッダー検証"]
  verify -->|OK| pass["設定成功"]
  verify -->|NG| fail["設定見直し"]

図のポイント: DevTools の Network タブでレスポンスヘッダーを確認することで、設定が正しく適用されているかを検証できます。

まとめ

本記事では、Nuxt アプリケーションを本番環境で安全に運用するためのセキュリティ設定を、チェックリスト形式で解説しました。

主要なポイントは以下の通りです。

  • セキュリティヘッダーの設定: nuxt.config.tsX-Content-Type-OptionsX-Frame-OptionsStrict-Transport-Security などのヘッダーを追加し、XSS やクリックジャッキング、中間者攻撃を防ぎます
  • Content Security Policy(CSP)の導入: リソースの読み込み元やスクリプトの実行を制限し、外部からの攻撃を防止します。まずは Report-Only モードで運用し、段階的に本番適用することをおすすめします
  • Cookie のセキュリティ属性設定: HttpOnlySecureSameSite 属性を適用し、セッションハイジャックや CSRF 攻撃からユーザーを守ります

これらの設定は、開発初期から組み込むことで、後からの修正コストを削減できます。セキュリティは機能実装と並行して進めるべき重要な要素ですので、本記事のチェックリストを参考に、ぜひ実装してみてください。

安全な Web アプリケーションを提供することで、ユーザーの信頼を獲得し、長期的なサービス運営が可能になるでしょう。

関連リンク