NestJS での CSRF-CSRF 実装ガイド
概要
このガイドでは、NestJS アプリケーションで CSRF 攻撃から保護するためのcsrf-csrf
ライブラリの実装方法について説明します。
csrf-csrf の利用背景
なぜ csrf-csrf を使用するのか
従来、Node.js アプリケーションで CSRF 保護に広く使用されていたcsurf
ライブラリは、非推奨(deprecated) となりました。これは主に以下の理由によるものです:
- メンテナンスの停止
- セキュリティアップデートの停止
- 新しい Node.js バージョンとの互換性問題
このため、NestJS コミュニティではcsrf-csrfライブラリが新たに推奨されています。
csrf-csrf の特徴
- Double Submit Cookieパターンを使用した CSRF 保護
- モダンなセキュリティベストプラクティスに準拠
- TypeScript フルサポート
- 軽量で高パフォーマンス
- アクティブなメンテナンス
実装方法
1. パッケージのインストール
npm install csrf-csrf
npm install -D @types/cookie-parser
必要な依存関係:
{
"dependencies": {
"csrf-csrf": "^4.0.3",
"cookie-parser": "^1.4.7"
}
}
2. main.ts での初期設定
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ValidationPipe } from "@nestjs/common";
import * as cookieParser from "cookie-parser";
import { doubleCsrf } from "csrf-csrf";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// その他の設定...
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
})
);
app.enableCors({
credentials: true,
origin: [process.env.FRONTEND_URL || "http://localhost:3003"],
});
// cookie-parserの設定(csrf-csrfの前に設定する必要がある)
app.use(cookieParser());
// CSRF保護の設定
const { doubleCsrfProtection } = doubleCsrf({
getSecret: () => {
const secret = process.env.CSRF_SECRET;
if (!secret) {
throw new Error("CSRF_SECRETの環境変数が設定されてません");
}
return secret;
},
getSessionIdentifier: (req) => req.ip || "anonymous", // セッションがない場合はIPアドレスを使用
cookieName:
process.env.NODE_ENV === "production"
? "__Host-psifi.x-csrf-token"
: "csrf-token", // 開発環境では __Host- プレフィックスを使わない
cookieOptions: {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
},
size: 32,
ignoredMethods: ["GET", "HEAD", "OPTIONS"],
});
// CSRF保護ミドルウェアを適用
app.use(doubleCsrfProtection);
await app.listen(process.env.PORT ?? 3000);
}
void bootstrap();
主要な設定項目の説明
-
getSecret
: CSRF トークン生成に使用する秘密鍵 -
getSessionIdentifier
: セッション識別子(IP アドレスやセッション ID を使用) -
cookieName
: CSRF クッキーの名前 -
cookieOptions
: クッキーのセキュリティ設定 -
size
: トークンのバイト数 -
ignoredMethods
: CSRF 保護をスキップする HTTP メソッド
3. auth.controller.ts での実装
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Req,
Res,
} from "@nestjs/common";
import { AuthDto } from "./dto/auth.dto";
import { AuthService } from "./auth.service";
import { Csrf, Msg } from "./interfaces/auth.interface";
import { Request, Response } from "express";
// CSRF対応のRequest型を定義
interface CsrfRequest extends Request {
csrfToken(): string;
}
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Get("csrf-token")
getCsrfToken(@Req() req: CsrfRequest): Csrf {
// csrf-csrfミドルウェアが実行された後、req.csrfToken()が利用可能になります
const token = req.csrfToken();
return { csrfToken: token };
}
@Post("signup")
signUp(@Body() dto: AuthDto): Promise<Msg> {
return this.authService.signUp(dto);
}
@HttpCode(HttpStatus.OK)
@Post("login")
async login(
@Body() dto: AuthDto,
@Res({ passthrough: true }) res: Response
): Promise<Msg> {
const jwt = await this.authService.login(dto);
res.cookie("access_token", jwt.accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "lax" : "none",
path: "/",
});
return { message: "SignIn OK" };
}
@HttpCode(HttpStatus.OK)
@Post("logout")
logout(@Req() req: Request, @Res({ passthrough: true }) res: Response): Msg {
res.clearCookie("access_token");
return {
message: "SignOut OK",
};
}
}
4. インターフェースの定義
auth/interfaces/auth.interface.ts
:
export interface Csrf {
csrfToken: string;
}
export interface Msg {
message: string;
}
POSTMAN での動作確認
Step 1: CSRF トークンの取得
-
リクエスト設定
- Method:
GET
- URL:
http://localhost:Port/auth/csrf-token
- Method:
-
レスポンス例
Step 2: CSRF トークンを使用した POST リクエスト
-
リクエスト例(Sign Up)
- Method:
POST
- URL:
http://localhost:Port/auth/signup
- Body:
{ "email": "test@example.com", "password": "testpassword123" }
- Method:
-
Cookie 設定
- POSTMAN の設定で自動的にクッキーが送信されるようにする
- または手動で Cookie ヘッダーを設定
Step 3: CSRF トークンなしでのテスト
CSRF トークンを送信せずに POST リクエストを送信し、403 Forbidden エラーが返されることを確認します。
セキュリティのベストプラクティス
1. 環境変数の設定
.env
ファイルに以下を追加:
CSRF_SECRET=your-super-secret-csrf-key-here-32-chars-minimum
NODE_ENV=development
FRONTEND_URL=http://localhost:Port
2. 本番環境での考慮事項
-
HTTPS 必須:
secure: true
でクッキーを HTTPS 限定にする - 強力な秘密鍵: 32 文字以上のランダムな文字列を使用
-
適切な SameSite 設定:
lax
またはstrict
を使用 - __Host-プレフィックス: 本番環境ではより安全なクッキー名を使用
3. フロントエンドでの実装
// CSRFトークンを取得
const getCsrfToken = async () => {
const response = await fetch("/auth/csrf-token", {
credentials: "include",
});
const data = await response.json();
return data.csrfToken;
};
// APIリクエスト時にCSRFトークンを送信
const makeSecureRequest = async (url, data) => {
const csrfToken = await getCsrfToken();
return fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-csrf-token": csrfToken,
},
credentials: "include",
body: JSON.stringify(data),
});
};
トラブルシューティング
よくある問題と解決方法
-
403 Forbidden エラー
- CSRF トークンがヘッダーに正しく設定されているか確認
- クッキーが正しく送信されているか確認
- ヘッダー名が
x-csrf-token
であることを確認
-
トークンが生成されない
-
cookie-parser
が csrf-csrf より前に設定されているか確認 - 環境変数
CSRF_SECRET
が設定されているか確認
-
-
CORS 問題
-
credentials: true
が設定されているか確認 - オリジンが正しく設定されているか確認
-
まとめ
csrf-csrf ライブラリを使用することで、NestJS アプリケーションに堅牢な CSRF 保護を実装できます。重要なポイントは:
- 適切な設定でミドルウェアを構成する
- フロントエンドで CSRF トークンを正しく処理する
- 本番環境でのセキュリティ設定を適切に行う
この実装により、CSRF 攻撃からアプリケーションを効果的に保護できます。