メインコンテンツへスキップ
kt-tech.blog
【設計】コードレビューで見つけたセキュリティ・並行処理の落とし穴と修正アプローチ
設計
(更新: 2026/3/20)· 約4分で読めます

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

Share
💡
チャットアプリの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("akari"));
if (!character) 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で不要なフェッチを防ぐ

この記事が役に立ったら共有しよう

Share
Koki

Koki

フルスタックエンジニア / React, Next.js, TypeScript