製造現場の「チョコ停」とWebサービスの「ReDoS」
工場の生産ラインにおいて、もっとも厄介なトラブルの一つに「チョコ停」があります。故障ではないのに、部材の詰まりやセンサーの誤検知で頻繁にラインが止まる現象です。原因は些細なことでも、1回数分の停止が1日に数十回積み重なれば、設備の総合設備効率(OEE)は数%〜十数%低下し、結果として莫大な生産損失を生みます。
Webサービスのバックエンド開発においても、これによく似た、しかしより致命的な現象が存在します。それがReDoS(Regular expression Denial of Service)です。
たった1行、わずか20文字程度の正規表現の不備によって、CPU使用率が100%に張り付き、サーバーが応答不能に陥る。悪意ある攻撃者が意図的に送り込んだ文字列だけでなく、時には一般ユーザーの入力さえも引き金になり得ます。
実務の現場で異常検知や予知保全のAIシステムを構築する際、この「予知保全」の考え方はソフトウェア開発にもそのまま適用できることがわかります。「動くからヨシ」ではなく、データから「いつか止まるかもしれない」リスクを定量的に把握し、事前に潰すアプローチです。
昨今、ChatGPTやGitHub CopilotなどのAIコーディング支援ツールが普及し、複雑な正規表現も一瞬で生成できるようになりました。しかし、ここに大きな落とし穴があります。AIは「機能要件(期待通りにマッチするか)」を満たすのは得意ですが、「非機能要件(高負荷時にも安全か)」については、明示的に指示しない限り無頓着なことが多いのです。
本記事では、AIが生成した(あるいは人間が書いた)正規表現の脆弱性を、あえてAI自身を使って暴き出す手法を解説します。AIを「コードを書くアシスタント」から「セキュリティホールを見つける監査役」へと役割を変えるのです。
これは、Webサービスの「品質予測」であり、サーバーダウンを防ぐための「デジタルな予知保全」の話です。
なぜ「AI任せの正規表現」がセキュリティホールになるのか
「AIに書いてもらったから大丈夫だろう」。そう思っていませんか?
実は、AIモデル(LLM)の学習データには、Web上に無数に存在する「動くけれど脆弱なコード」も大量に含まれています。そのため、AIは悪気なく、セキュリティリスクのある正規表現を提案してくることがあります。
便利さの裏に潜む「ReDoS」の脅威
ReDoSとは、正規表現エンジンの仕様(特にバックトラック処理)を悪用し、処理時間を指数関数的に増大させる攻撃手法です。
例えば、メールアドレスのバリデーションや、特定のフォーマットを持つIDの抽出などで、少し複雑な正規表現を使うことは日常茶飯事でしょう。もしその正規表現に構造的な欠陥があると、特定のパターンの入力文字列(攻撃文字列)を与えられた際、検証処理が終わらずにサーバーのリソースを食いつぶしてしまいます。
Node.jsのようなシングルスレッドのランタイムでは、たった一つのリクエストがイベントループをブロックし、サービス全体を停止させてしまうことさえあります。これは、製造ラインで言えば、たった一つのセンサーの誤作動でメインコンベアが緊急停止し、工場全体の稼働率を著しく押し下げるようなものです。
AIは「動くコード」を書くが「安全なコード」とは限らない
AIに「メールアドレスを検証する正規表現を書いて」と頼むと、一般的なRFC準拠の(あるいはそれに近い)複雑な正規表現を出力してくれます。しかし、その正規表現が「ReDoSに対して安全か」まで考慮されている保証はありません。
AIは確率的に「もっともらしい」回答を生成しているに過ぎず、そのコードが最悪のケースでどのような計算量になるか(O(n)なのかO(2^n)なのか)を厳密に計算してから出力しているわけではないからです。
特に、「任意の文字の繰り返し」を含むパターンや、グループ化が入れ子になっているパターンにおいて、AIはしばしば脆弱なコードを生成します。開発者が「AIが書いたんだから正しいはずだ」と盲信し、そのままプロダクション環境にデプロイしてしまう。これが現代のセキュリティホールの新たな発生源となっています。
たった一行でサービス停止に追い込まれるメカニズム
ReDoSの恐ろしさは、その手軽さと破壊力の非対称性にあります。
攻撃者は高度なハッキングツールを使う必要はありません。脆弱な正規表現を使っている入力フォームに、数十文字から数百文字程度の「細工された文字列」を送信するだけです。
例えば、以下のような単純に見える正規表現でも、ReDoSの餌食になります。
(a+)+$
これに対し、aaaaaaaaaaaaaaaaaaaaX (aが並んで最後にXが来る)といった文字列を与えると、多くの正規表現エンジンはパニックに陥ります。文字数が増えるごとに計算時間が倍々ゲームで増え、あっという間に数分、数時間、あるいは永遠に処理が終わりません。
AIは文脈に応じてコードを生成するため、プロンプト次第ではこのような「素朴な実装」を提案してくる可能性が十分にあります。だからこそ、受け取る人間側に「検品」のスキル、あるいは「検品する仕組み」が必要なのです。
正規表現の「バックトラック」を理解する
敵を知り己を知れば百戦危うからず。まずは、なぜ正規表現が暴走するのか、そのメカニズムをエンジニアリングの視点で分解してみましょう。
エンジン内部で何が起きているのか
多くのプログラミング言語(JavaScript, Python, Ruby, Javaなど)の標準的な正規表現エンジンは、NFA(非決定性有限オートマトン)ベースの実装を採用しています。このエンジンは、マッチングに失敗すると、直前の分岐点まで戻って別の可能性を試そうとします。これを「バックトラック(後戻り)」と呼びます。
迷路探索をイメージしてください。行き止まりに当たったら、一つ前の分かれ道まで戻って別の道を行く。これをゴールが見つかるか、全ての道を試し尽くすまで繰り返します。
通常の使用では、このバックトラックは有用な機能です。しかし、正規表現の書き方によっては、この「分かれ道」が爆発的に増えてしまうことがあります。
「邪悪な正規表現(Evil Regex)」の構造パターン
ReDoSを引き起こしやすい正規表現パターンは、通称「Evil Regex」と呼ばれます。主な特徴は以下の3点です。
- 繰り返しの繰り返し:
(a+)+のように、量指定子(+, *, {n,}など)が適用されたグループ自体に、さらに量指定子がついている。 - 重複した分岐:
(a|a)+のように、同じ文字にマッチする選択肢が複数ある。 - 接尾辞の不一致: 最後にマッチしない文字(
$など)があり、エンジンに「全ての可能性を試させる」ことを強制する。
これらの構造があると、入力文字列の長さ $n$ に対して、試行回数が $2^n$ (指数関数的)に増加する場合があります。$n=30$ 程度でも、処理回数は10億回を超え、CPUは悲鳴を上げます。
人間には見抜きにくい入れ子構造の罠
問題は、実際の開発現場で使われる正規表現は、(a+)+ のように単純ではないことです。
^([a-zA-Z0-9]+[._-]?)+[a-zA-Z0-9]+@[a-zA-Z0-9]+.[a-zA-Z]{2,}$
一見すると普通のメールアドレス検証用に見えますが、前半の ([a-zA-Z0-9]+[._-]?)+ の部分に危険な匂いがします。+ の中に + や ? が潜んでおり、特定の入力に対して大量のバックトラックが発生する可能性があります。
こうした複雑なパターンの脆弱性を、人間の目視レビューだけで見抜くのは至難の業です。熟練のエンジニアでも見落とします。だからこそ、ここでAIの出番となるのです。ただし、「生成」ではなく「検証」のために。
発想の転換:AIを「生成係」から「監査係」へ
製造業へのAI導入において、「AIに答えを出させるな、AIに間違いを探させろ」というアプローチが有効なケースが多く見られます。生成系AIは、創造的なタスクよりも、パターン認識や多角的な視点からの批判的レビューにおいて、驚くべき性能を発揮することがあるからです。
正規表現のセキュリティ対策においても、このアプローチは極めて有効です。
AIのハルシネーションを逆手に取るセキュリティテスト
AIに正規表現を作らせると、たまに間違ったコード(ハルシネーション)を出力します。しかし、逆に「この正規表現を攻撃するための文字列を作れ」と命じると、AIはその豊富な知識ベースから、ReDoS攻撃に使われそうなパターンを推測して生成しようとします。
これは、いわば「AIによる一人レッドチーム演習」です。自分が書いた(あるいはAIに書かせた)コードに対して、AIを攻撃者役としてぶつけるのです。
攻撃者の視点をAIにシミュレートさせる
人間がテストケースを考えると、どうしても「正常系(正しくマッチするケース)」や「単純な異常系(フォーマット違反)」に偏りがちです。「正規表現エンジンをクラッシュさせるための文字列」を即座に思いつく開発者は稀でしょう。
AIであれば、「この正規表現の計算量が最大になるような入力文字列」という抽象的な指示から、具体的な攻撃ベクトルを導き出すことができます。
網羅的なエッジケースの洗い出し
また、AIは疲れません。人間なら3つも考えれば十分と思うところを、AIなら「他に考えられる脆弱なパターンを10個挙げろ」と言えば、文句も言わずに列挙してくれます。
この「執拗さ」こそが、セキュリティ監査に必要な資質です。AIをパートナーにすることで、個人の経験則に依存しない、網羅的な脆弱性チェックが可能になります。
【実践】ReDoS脆弱性を検知する検証用プロンプト設計
では、具体的にどのようなプロンプトを使えばよいのでしょうか。現場での検証に有効なプロンプトのテンプレートを紹介します。ChatGPTやClaudeの最新モデルなど、複雑な論理推論に長けたAIモデルでの使用を推奨します。
計算量を推測させるプロンプトテンプレート
まずは、対象の正規表現にReDoSのリスクがあるかどうかを診断させます。IDE統合型AI(GitHub Copilot等)を使用している場合は、対象のコードを選択した状態で以下の指示を出すか、@workspace コマンドを活用して文脈全体を読み込ませるとより精度が高まります。
# Role
あなたは熟練したセキュリティエンジニアです。正規表現の脆弱性診断、特にReDoS(Regular expression Denial of Service)の解析を専門としています。
# Request
以下の正規表現について、ReDoS脆弱性の有無を詳細に分析してください。
## 対象の正規表現
```regex
{ここに検証したい正規表現を貼り付け}
分析項目
- 構造解析: ネストされた量指定子や重複した分岐など、Evil Regexのパターンが含まれているか。
- 計算量推定: 最悪ケースにおいて、入力文字数 $n$ に対して計算量はどのように増加するか(例: $O(n)$, $O(n^2)$, $O(2^n)$)。
- 攻撃可能性: どのような入力パターンを与えればバックトラックが大量発生するか。
- 修正案: 機能を維持しつつ、ReDoS脆弱性を排除した安全な正規表現(Atomic Groupingの利用など)。
Output Format
分析結果は、エンジニアが理解しやすいように技術的な根拠と共に記述してください。
このプロンプトのポイントは、単に「安全か?」と聞くのではなく、「計算量推定」と「構造解析」を求めている点です。これにより、AIは論理的に正規表現の構造を分解し、リスクを評価しようと試みます。
### 「最悪のケース」を具体的に生成させる指示
脆弱性の疑いがある場合、実際にテストするための攻撃用文字列(PoC: Proof of Concept)を生成させます。
```markdown
# Request
上記の正規表現に対して、正規表現エンジンに最大の負荷をかける「攻撃用文字列(PoC)」を作成してください。
## 要件
- バックトラックを最大限に発生させるパターンであること。
- 文字列の長さは20文字〜50文字程度で、段階的に長くした例を3つ提示すること。
- なぜその文字列が負荷を高めるのか、エンジンの挙動(バックトラックの発生箇所)を解説すること。
この出力を使って、ローカル環境(本番環境では絶対にやらないでください!)で実際に正規表現を実行し、処理時間を計測します。もし20文字程度で数秒かかるようなら、その正規表現は即座に修正が必要です。小さくテストを始めてリスクを可視化することが、確実な改善への第一歩となります。
安全な代替案(Atomic Grouping等)を提案させる
脆弱性が見つかった場合、修正案もAIに出させましょう。この際、モダンな正規表現エンジンの機能活用を促すと効果的です。最新のAIコーディングアシスタントであれば、エージェント機能を通じて修正案を直接コードベースに適用し、リファクタリングを行うことも可能です。
# Request
このReDoS脆弱性を修正するための改善案を提示してください。
## 制約条件
- もとの正規表現が意図していたマッチング仕様(機能要件)を変更しないこと。
- 以下のいずれかの手法を検討すること。
1. Atomic Grouping (?>...) や **Possessive Quantifiers (*+, ++) の使用(使用可能な言語の場合)。
2. 正規表現のリファクタリング(ネストの解消)。
3. 入力文字列長の制限など、正規表現以外のアプローチ。
AIは「肯定的先読み (?=...)」などを駆使して、バックトラックを防ぐテクニカルな書き換え案を提案してくれます。GitHub Copilotなどのツールを使用している場合は、提示された修正案に対してテストコードの生成も依頼し、挙動が変わっていないことを確認するワークフローを推奨します。
開発フローへの組み込みと安全な運用
プロンプトによる検証は強力ですが、毎回手動でチャットに入力するのは手間ですし、抜け漏れも発生します。製造業で言う「標準作業手順書(SOP)」に組み込み、システム化することが重要です。
CI/CDパイプラインでの自動チェックの可能性
AIによるチェックを開発プロセスの一部に統合しましょう。
- コードレビュー時のチェックリスト: プルリクエストのテンプレートに「正規表現を追加・変更した場合は、ReDoS検証プロンプトで確認したか?」という項目を追加する。
- Linter/静的解析ツールの導入: AIではありませんが、
eslint-plugin-regexpやSemgrepなどのツールは、既知のReDoSパターンを検知できます。これらをCIで回しつつ、ツールですり抜ける複雑なロジックをAIで人間がレビューするという「多層防御」が理想です。
チームで共有すべき「正規表現ホワイトリスト」
毎回ゼロから正規表現を作るのではなく、検証済みの安全な正規表現をライブラリ化(ホワイトリスト化)し、チームで共有することを推奨します。
「メールアドレス検証はこれを使う」「日付形式はこれを使う」と決めておけば、個々の開発者が誤って脆弱なコードを混入させるリスクを減らせます。製造業で言う「共通部品化」です。
万が一のタイムアウト設定という命綱
どれだけ事前検証しても、見落としはゼロにはなりません。最後の砦として、ランタイムレベルでの防御策が必要です。
正規表現の実行には必ずタイムアウト(Timeout)**を設定してください。
- Node.js:
vmモジュールを使ってサンドボックス内で実行しタイムアウトを設定する、あるいはvalidator.jsなどの安全なライブラリを使用する。 - Python:
reモジュールにはタイムアウトがないため、サードパーティ製のregexモジュールを使用するか、シグナルハンドラ等で処理時間を制限する。 - Java/C#: 標準ライブラリでタイムアウトを設定可能な場合が多いので、必ず指定する。
「処理が100ミリ秒を超えたら強制終了してエラーを返す」。この単純なルール一つが、サーバーダウンという最悪の事態を防ぐ安全装置(フェイルセーフ)となります。
まとめ:AI時代の「守り」の技術
AIによるコード生成は、開発速度を劇的に向上させました。しかし、速度と引き換えに、私たちが把握しきれない「ブラックボックス」なコードが増えているのも事実です。
ReDoSのような脆弱性は、平時には目立たず、アクセス集中時や悪意ある攻撃を受けた瞬間に牙を剥きます。だからこそ、事前の検知と対策が不可欠です。
- AI生成コードを盲信しない: 「機能要件」だけでなく「非機能要件」も満たしているか疑う。
- バックトラックの恐怖を知る: 指数関数的な計算量爆発のメカニズムを理解する。
- AIを監査役に任命する: 脆弱性診断プロンプトを活用し、攻撃者の視点でコードをテストする。
- フェイルセーフを用意する: タイムアウト設定で万が一の暴走を食い止める。
AIを「攻撃者」に見立てて脆弱性を洗い出すプロセスは、まさにデジタル時代の避難訓練です。この新しい習慣を取り入れ、継続的な改善を推進することで、堅牢で信頼性の高いシステムを構築してください。
コメント