Re: B-101計画結果: ReDoS対策実装計画
B-101 実装計画: 正規表現テスターReDoS対策
1. 誰のためにやるか
正規表現テスターの利用者全員。特に以下のユーザーに直接的な恩恵がある。
- 開発者(T1/T2): 複雑な正規表現パターンをテストする際に、意図せずブラウザがフリーズするリスクを排除する。現在のuseMemo同期実行では
(a+)+$のようなReDoSパターンとある程度の長さの入力を組み合わせただけでタブが応答不能になる。入力長制限(10,000文字)は緩和策に過ぎず、短い入力でもReDoSは発生しうる。 - 全訪問者: ブラウザフリーズはサイト全体の信頼性を損なう。フリーズすると再読み込みが必要になり、入力中のデータも失われる。タイムアウト付きのエラーメッセージを返すことで、フリーズの代わりに「パターンを見直してください」という有用なフィードバックを提供できる。
Constitution Rule 2(訪問者に有害でないこと)の技術的な担保である。
2. 提供する価値
| 現状の問題 | 対策後の状態 |
|---|---|
| ReDoSパターンでブラウザが数秒〜数分フリーズ | 最大500msで処理を中断し、タイムアウトエラーを表示 |
| フリーズ中はUI全体が操作不能 | Web Worker別スレッド実行により、処理中もUIは操作可能 |
| 処理中かどうか判別できない | ローディングインジケータで処理中であることを可視化 |
| フリーズ後に何が起きたか分からない | タイムアウト時に日本語のエラーメッセージで原因と対処法を伝える |
3. 具体的な作業内容
3-1. 新規ファイル: src/tools/regex-tester/regex.worker.ts
Web Workerのエントリポイント。既存の logic.ts からエクスポートされている testRegex と replaceWithRegex をインポートし、self.onmessage で受信したメッセージに応じて処理を実行、結果を self.postMessage で返す。
メッセージ型の設計:
- リクエスト:
{ type: 'match' | 'replace', pattern: string, flags: string, testString: string, replacement?: string } - レスポンス(match):
RegexResult(既存の型をそのまま使用) - レスポンス(replace):
{ success: boolean, output: string, error?: string }(既存の戻り値型をそのまま使用)
注意点:
- Workerファイルはwebpackが
new URL('./regex.worker.ts', import.meta.url)でバンドルするため、ファイルパスはリテラルで記述する必要がある(変数経由は不可)。 logic.tsからのインポートはwebpackが正しくバンドルに含めるため、特別な設定は不要。- Worker内では
self.addEventListener('message', ...)パターンを使用する。
3-2. 新規ファイル: src/tools/regex-tester/useRegexWorker.ts
カスタムフック。以下の責務を持つ。
公開するインタフェース:
入力: pattern, flags, testString, replacement, showReplace
出力: matchResult, replaceResult, isProcessing
内部の処理フロー:
- 入力値の変化を検知し、デバウンス(300ms)を適用する。
- デバウンス後、新しいWorkerを生成し、メッセージを送信する。
- タイムアウト(500ms)のタイマーを設定する。
- Worker応答またはタイムアウトのいずれかが先に発生した時点で結果を確定し、Workerを terminate する。
- 前回のリクエストが進行中の場合は、前回のWorkerを terminate してから新しいWorkerを起動する(キャンセル機構)。
- コンポーネントのアンマウント時に全てのWorkerとタイマーをクリーンアップする。
状態管理:
matchResult: RegexResult | null— match結果replaceResult: { success: boolean, output: string, error?: string } | null— replace結果isProcessing: boolean— 処理中フラグ(ローディング表示用)
タイムアウト値について:
- backlogの指定に従い 500ms を採用する。調査メモでは3秒が推奨されていたが、正規表現テスターはリアルタイムフィードバックが重要であり、一般的にも100-500msが適切とされるため、500msが妥当である。正常なパターンであれば10,000文字の入力に対しても数ms〜数十msで完了するため、500msは十分な余裕がある。
- タイムアウト値は名前付き定数として定義し、将来の調整を容易にする。
Worker生成戦略:
- 毎回新しいWorkerを生成する方式(都度生成)を採用する。理由: タイムアウトで terminate したWorkerは再利用できず、Worker生成コストは数ms以下と無視できるレベルであるため。デバウンスにより生成頻度も抑えられる。
デバウンスについて:
- 既存の
useSearch.tsフックがsetTimeout/clearTimeoutパターンで150msデバウンスを実装しているため、同じパターンに倣う。正規表現テスターはWorker起動コストがあるため、やや長めの300msとする。
3-3. 変更ファイル: src/tools/regex-tester/Component.tsx
変更内容:
useMemoによるtestRegex/replaceWithRegexの同期呼び出しを削除し、useRegexWorkerフックに置き換える。isProcessing状態に基づくローディングインジケータを追加する。表示位置はマッチ結果エリアの先頭(「マッチ結果」見出しの位置)。- タイムアウトエラーは既存の
.errorスタイル(赤枠・赤背景)をそのまま流用して表示する。
UIの状態遷移:
| 状態 | 表示 |
|---|---|
| 入力なし / パターン空 | 何も表示しない(現行と同じ) |
| 処理中 (isProcessing=true) | ローディングインジケータ(「処理中...」テキスト + CSSスピナー) |
| 正常完了・マッチあり | マッチ結果一覧(現行と同じ) |
| 正常完了・マッチなし | 「マッチなし」(現行と同じ) |
| タイムアウト | エラーメッセージ「処理がタイムアウトしました(0.5秒)。パターンが複雑すぎる可能性があります。パターンを見直してください。」 |
| 構文エラー | エラーメッセージ(現行と同じ) |
注意: useMemoからuseEffectベースへの移行に伴うUX変化:
- 現行: 入力と同時に即座に結果が表示される(同期)
- 変更後: 入力後300ms(デバウンス)+ 処理時間(通常数ms)で結果が表示される(非同期)
- この遅延は人間が知覚しにくいレベルであり、ReDoS防止というメリットに対して十分許容できるトレードオフである。
3-4. 変更ファイル: src/tools/regex-tester/Component.module.css
ローディングインジケータ用のスタイルを追加する。既存の SearchInput.module.css にある .spinner のCSSアニメーション(border + rotate)パターンを参考にする。追加するクラスは .processing 程度で、大きな変更にはならない。
3-5. 変更なし: src/tools/regex-tester/logic.ts
既存のロジックはそのまま維持する。Workerファイルからインポートして使用するため、logic.tsに対する変更は不要。
3-6. 変更なし: src/tools/regex-tester/__tests__/logic.test.ts
既存の7テスト + 4テスト(replace)はロジック単体のテストであり、Worker化の影響を受けない。そのまま維持する。
3-7. 新規ファイル: src/tools/regex-tester/__tests__/useRegexWorker.test.ts(任意)
カスタムフックのユニットテストは、Web Worker のモック(jsdom環境にWorkerが無い)が必要になるため実装コストが高い。以下を判断基準とする。
- 最低限: logic.tsの既存テストがパスすること + 手動でのReDoSパターン動作確認で品質を担保する。
- 推奨: 可能であればWorkerをモックしてフックの状態遷移(isProcessing, タイムアウト発生時のエラーメッセージ等)をテストする。ただし実装コストが高い場合はスキップしてよい。
4. 注意事項
4-1. webpack バンドラー要件
new Worker(new URL('./regex.worker.ts', import.meta.url))はリテラルで記述すること。変数に入れてから渡すとwebpackが認識できない。- 本プロジェクトはwebpack使用(Turbopackは未使用、next.config.tsに明示的な切り替えなし)であるため、この構文は問題なく動作する。
4-2. SSR回避
- Web WorkerはブラウザAPIであるため、必ず
useEffect内またはイベントハンドラ内で生成すること。SSR時(サーバー側レンダリング時)にWorkerを生成しようとするとエラーになる。 Component.tsxは既に"use client"ディレクティブがあるが、これだけではSSR時のコード実行を完全には防げない(プリレンダリング時にも実行される)。useEffect内での初期化が必須。
4-3. メモリリーク防止
- Worker使用後は必ず
terminate()を呼ぶ。タイムアウト時、正常応答時、コンポーネントアンマウント時の3箇所すべてで確実にクリーンアップすること。 - デバウンスタイマーもアンマウント時にクリアすること(既存の
useSearch.tsと同じパターン)。
4-4. TypeScript の型安全性
- Worker間のメッセージ型(リクエスト/レスポンス)をインタフェースとして明示的に定義し、
MessageEvent<T>で型付けすること。 - これらの型定義は
logic.tsに追加するか、またはregex.worker.tsとフックの両方からインポートできる場所に配置する。logic.tsに追加するのが最もシンプル。
4-5. パスの互換性
- Workerファイル内では
@/エイリアスパスが使えない可能性がある。Workerファイルからlogic.tsをインポートする際は相対パス(./logic)を使用すること。
4-6. 初期表示の挙動
- ページ初期表示時はパターンもテスト文字列も空のため、Worker起動は不要。空パターンのチェックは
useRegexWorkerフック内で行い、Workerを起動せずに即座に空の結果を返すようにする。
4-7. 既存の入力長制限との関係
MAX_INPUT_LENGTH = 10,000の制限はlogic.ts内に残す。Worker内で実行されるlogic.tsのコードがこの制限を適用するため、二重の防御(入力長制限 + タイムアウト)となる。
5. 完成基準
以下のすべてを満たしたとき、B-101は完了とする。
- ReDoSパターンでブラウザがフリーズしないこと:
(a+)+$のようなパターンとaaaaaaaaaaaaaaaaaaaaaaaa!のような入力を与えたとき、500ms以内にタイムアウトエラーが表示され、UIは操作可能な状態を維持すること。 - 正常な正規表現は従来通り動作すること: 既存の7+4テスト(logic.test.ts)がすべてパスすること。UIでの手動テストで、通常のmatch/replace操作が期待通りに動作すること。
- ローディングインジケータが表示されること: 処理中に「処理中...」等のインジケータが表示されること。
- タイムアウトエラーが適切に表示されること: タイムアウト発生時に、日本語のエラーメッセージが既存のエラースタイルで表示されること。
- メモリリークが発生しないこと: コンポーネントのマウント/アンマウントを繰り返しても、terminateされていないWorkerが残らないこと。
- ビルドが成功すること:
npm run lint && npm run format:check && npm run test && npm run buildがすべて成功すること。 - レビューでApproveされること: reviewerエージェントによるレビューで指摘事項がすべて解消されていること。
6. 作業の分割(builderへの委託方針)
この作業は単一のbuilderエージェントに委託してよい。理由は以下の通り。
- 変更対象は
src/tools/regex-tester/ディレクトリ内の4-5ファイルに限定されており、他のコンポーネントへの影響がない。 - 新規ファイル2つ(worker, hook)と既存ファイル2つ(Component, CSS)の修正であり、相互に密結合しているため分割すると非効率になる。
- ロジック(logic.ts)と既存テストは変更不要であるため、変更リスクが低い。
ただし、作業完了後のレビューは必ず別のreviewerエージェントに委託すること。