エラーセキュリティ は何をしますか?
ルール
- 本番環境では、スタックトレース、データベースエラー、ファイルパスをクライアントに絶対に漏洩させない
- 一意のエラーIDと共に汎用的なエラーメッセージを返す:
{ error: "問題が発生しました", errorId: "abc123" }
- デバッグのために、同一の相関IDを使用してサーバーサイドに完全なエラー詳細を記録する
- 異なるエラー詳細レベルを使用: 開発環境(完全なスタックトレース)vs 本番環境(安全なメッセージ+ID)
- グローバルエラーハンドラーで未処理のエラーをすべてキャッチする — 素のエラーがクライアントに届かないようにする
- サードパーティサービスからのエラーメッセージをクライアントに転送する前にサニタイズする
- 適切なHTTPステータスコードを返す: 400(不正な入力)、401(認証なし)、403(禁止)、404(見つからない)、500(サーバーエラー)
import { randomUUID } from "node:crypto";
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
const errorId = randomUUID();
console.error({ errorId, message: err.message, stack: err.stack, url: req.url });
const statusCode = (err as any).statusCode || 500;
res.status(statusCode).json({
error: statusCode >= 500 ? "サーバー内部エラー" : err.message,
errorId,
...(process.env.NODE_ENV === "development" && { stack: err.stack }),
});
});
'use client';
import { useEffect } from 'react';
export default function GlobalError({ error, reset }: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error('Global error:', error);
}, [error]);
return (
<div>
<h2>問題が発生しました</h2>
<p>エラーID: {error.digest}</p>
<button onClick={() => reset()}>再試行</button>
</div>
);
}
避けるべきこと
err.message を直接返す — データベースエラーはテーブル名、カラム名、クエリ構造を漏洩する
- 本番環境のレスポンスでスタックトレース — ファイルパス、依存関係、内部アーキテクチャを露呈する
- すべてに汎用的な500を使用する — クライアントがエラーを適切に処理できるように適切なステータスコードを使用する
- 相関IDなしでエラーをログに記録する — 本番環境のデバッグがほぼ不可能になる
- エラーを静かに握りつぶす — レスポンスが汎用的でも、必ずログに記録する