【設計】コードレビューで見つけたセキュリティ・並行処理の落とし穴と修正アプローチ
設計約5分で読めます

この記事でわかること
- DB に入っている値をファイルパスに使うときの Path Traversal と、path.basename() での回避
- async 処理の途中で useRef の値が入れ替わり、別セッションに書き込む Race Condition
- early return だけで済ませるとユーザーが詰む理由
- useQuery の enabled を忘れたときに起きること
React(hooks)と TypeScript を書いたことがあること。Next.js のコードを例にしています。
メモ
チャットアプリのPRレビューで発見した、見落としやすいセキュリティ脆弱性と並行処理のバグ、その修正パターンを紹介します。
1. Path Traversal — DBの値を信用しない
問題
// ❌ DBから取得したファイル名をそのままpath.joinに渡している
const promptFileName = character.promptFile;
const promptPath = path.join(process.cwd(), "src/prompts", promptFileName);
fs.readFileSync(promptPath, "utf-8");
// character.promptFile が "../../etc/passwd" だったら?修正
// ✅ path.basename() でディレクトリトラバーサルを防止
const promptFileName = path.basename(character.promptFile);
const promptPath = path.join(process.cwd(), "src/prompts", promptFileName);重要
path.basename() は "../../etc/passwd" から "passwd" だけを取り出します。DB の値だから安全、とは限りません。そのレコードを入れたのが管理画面やインポート処理なら、元をたどれば外部入力です。ファイルパスに使う値は出所を問わず正規化するのが安全です。2. Race Condition — useRefの値がasync中に変わる
問題
// ❌ async処理中にsessionIdRef.currentが別のセッションに上書きされる
const sendMessage = useCallback(async (content: string) => {
if (!sessionIdRef.current) return;
await saveMessage(sessionIdRef.current, "user", content);
const { text } = await getChatResponse(content); // 数秒かかる
// ↑ この間にキャラ変更 → sessionIdRef.current が変わる
await saveMessage(sessionIdRef.current, "assistant", text); // 別セッションに保存!
}, []);修正
// ✅ 関数冒頭でローカル変数にコピー
const sendMessage = useCallback(async (content: string) => {
const currentSessionId = sessionIdRef.current; // ここで固定
if (!currentSessionId) return;
await saveMessage(currentSessionId, "user", content);
const { text } = await getChatResponse(content);
await saveMessage(currentSessionId, "assistant", text); // 安全
}, []);3. サイレント失敗 — エラーなのにUIが何も言わない
問題
// ❌ キャラが見つからない → early return → ユーザーには何も見えない
const character = await getCharacterBySlug(selectedSlug);
if (!character) return; // 永遠にチャットが使えない状態に修正
// ✅ デフォルトキャラにフォールバック
const character =
(await getCharacterBySlug(selectedSlug)) ??
(await getCharacterBySlug(DEFAULT_CHARACTER_SLUG));
if (!character) return; // 両方なければ諦める(ありえないはず)Tips
if (!x) return; は書く側には楽ですが、ユーザーから見ると「何も起きない」ので、壊れているのか待てばいいのか判断できません。フォールバックするか、せめてエラーを画面に出すかのどちらかにしましょう。4. useQueryのenabled未設定 — 不要なリクエスト
// ❌ session未取得時にもクエリが走る
const { data } = useQuery({
queryKey: ["characters", session?.user?.id],
queryFn: () => getCharacters(session?.user?.id), // undefined で呼ばれる
});
// ✅ enabled で制御
const { data } = useQuery({
queryKey: ["characters", session?.user?.id],
queryFn: () => getCharacters(session?.user?.id),
enabled: !!session?.user?.id, // userIdがあるときだけ
});まとめ
- DBの値でもファイルパスに使うなら path.basename() で正規化する
- async関数内でuseRefを使うなら、冒頭でローカル変数にコピーする
- early returnにはフォールバックを用意し、ユーザーが詰む状態を作らない
useQueryのenabledで不要なフェッチを防ぐ
参考リンク
更新履歴
- コード例に入っていたプロダクト固有の名前を定数に差し替え。「DB の値なら安全」とは限らない理由を補足。early return がユーザーからどう見えるかの説明を追加。callout の色指定を他記事と揃え、メソッド名をインラインコードに統一。参考リンクの節を新設。


