Dev Study
← Webを支えるソフトウェア
レッスン 12 / 13·約 9 分·第3期 Webを支えるソフトウェア

WAFの誤検知 — なぜ正しい通信が止まるのか

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口今ここ
  6. アプリ処理
  7. DB保存

第37週はWAFが何を見てどう判定するかでした。今回は、その判定が正しい通信を止めてしまう場合を扱います。

まず結論

CRSは相手のアプリを知らないまま汎用のパターンで検出するため、正常な通信が攻撃に見えることが構造的に避けられません。これを誤検知(false positive)と呼びます。

直し方はルール本体を書き換えることではなく、除外ルール(rule exclusion)を外から足すことです。置き場所を間違えると効きません。

? 読む前に予測

利用者がパスワードを `/bin/bash` に設定していました。CRSがこれを攻撃と誤検知した場合、ログイン処理が止まる以外に、どんな問題が起きるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

汎用であることの代償

CRSは、守る相手のアプリを知りません。公式の言葉では「A fresh CRS deployment has no awareness of the web services that may be running behind it」。汎用の攻撃パターンだけで検出するので、正規の入力がたまたまそのパターンに一致することが起こります。

ブログの投稿欄に <h1> を含む本文を書けば、パラノイアレベル2ではHTMLタグを探すルールに一致します(公式の例)。入力は正しく、ルールも壊れていません。それでも遮断されます。誤検知はバグではなく、汎用性の代償です。

具体実物

直し方 — ルールには触らない

まず、やってはいけない方法から。公式は「Making direct modifications to CRS rule files is a bad idea and is strongly discouraged」と述べます。直接編集はルールセットのフォークに等しく、更新のたびに手作業でやり直す羽目になるからです。

正しい方法は除外ルールを外から足すことです。除外には2つの軸があり、組み合わせで4通りになります。

除外ルールの2軸4分類(公式の表)表
ルール丸ごと外す 変数だけ外す
設定時 SecRuleRemoveById SecRuleUpdateTargetById
SecRuleRemoveByTag SecRuleUpdateTargetByTag
実行時 ctl:ruleRemoveById ctl:ruleRemoveTargetById
ctl:ruleRemoveByTag ctl:ruleRemoveTargetByTag

先の投稿欄の例なら、ルールごと外すのではなくそのパラメータだけを対象から外します。他の入力への保護は残ります。

✓ ここまでの確認

特定のルールが誤検知を起こしています。最も適切な対処はどれですか。

抽象原理

置き場所を逆にすると、黙って効かない

ここが実務で刺さる点です。公式は「It is crucial to put rule exclusions in the correct place, otherwise they may not work」と強調しています。

理由は動詞を見れば分かります。設定時の除外はルールを「削除」するので、削除される側が先に定義されていなければなりません。だからCRSの後に置きます。実行時の除外はルールを「変更」するので、そのルールが実行される前でなければ意味がありません。だからCRSの前に置きます。

CRSは REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example(実行時用)と RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf.example(設定時用)を同梱しており、末尾の .example を外して使います。ファイル名の BEFORE / AFTER がそのまま置き場所を表しています。

逆に置いても、エラーにはなりません。黙って効かないだけです。誤検知が直らないとき、除外の書き方ではなく置き場所を疑う必要があります。

⇄ 2つを見比べる
具体例 A

WAFのパラノイアレベルと誤検知

レベルを上げると検出できる攻撃は増えますが、正規の通信を攻撃と判定する率も上がります。公式は「上げれば誤検知が増えるのは非常に起こりやすい」と明言しており、増えた分は除外ルールを書いて個別に打ち消します。

レベルと誤検知の関係(公式の説明より)表
PL1 誤検知はほとんど起きない。既定
PL2 実利用者のデータを扱う水準。誤検知は出る前提
PL3 誤検知が多い。除外を書けることが前提
PL4 極めて厳しく、大量の誤検知を覚悟する
具体例 B

CPUの分岐予測(第6週)

第6週で見たとおり、CPUは条件が確定する前に行き先を予測して進みます。外れれば進めた分を捨てて15〜20サイクル失いますが、毎回待つよりは速いので、外す前提で予測しています。

予測が外れたときの扱い表
当たり そのまま継続。損失ゼロ
外れ 進めた分を破棄して詰め直し(15〜20サイクル)
方針 外れを無くすのではなく、外れた分を払う

パラノイアレベルと分岐予測。片方はセキュリティ、もう片方はCPUの話ですが、設計の考え方に共通するものがあります。それは何で、代価はどこで払われているでしょうか。

「外れをゼロにしない」と決めた結果、何を得て、そのぶん何を負担しているかを比べてください。

✓ ここまでの確認

パラノイアレベルを上げたいが、誤検知で正規利用者を止めたくありません。公式が勧める手順はどれですか。

この章のまとめ

  • 誤検知は汎用性の代償。CRSは背後のアプリを知らないまま検出するので、正規の入力が攻撃パターンに一致しうる。
  • 直し方は除外ルール。CRSのファイルを直接編集しない(フォークになり、更新のたびにやり直しになる)。
  • 除外は2軸4分類。設定時/実行時 × ルール丸ごと/変数だけ。誤検知した入力だけを外せば保護は残る。
  • 置き場所を逆にすると黙って効かない。 設定時は削除なのでCRSの後、実行時は変更なのでCRSの前。
  • レベルを上げるときは閾値ではなく tx.detection_paranoia_level を先に上げる。閾値を上げる方法は公式が「risky」と呼ぶ。
  • 放置してはいけない理由は3つ。アラート疲れ / パスワードが平文でログに残る / 利用者体験の悪化。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 4正解: 0

CRSで誤検知が構造的に避けられない理由はどれですか。

用語集(5語)
誤検知 (false positive)
正規のやりとりがルールに誤って一致すること。公式は「When a genuine transaction causes a rule from CRS to match in error」と定義し、除外ルールで調整すべきものとしている。
除外ルール (rule exclusion, RE)
CRSのファイルを書き換えずに、外からルールの適用を取り消したり対象を狭めたりする仕組み。ルールセットをそのまま保てるので、更新が容易になる。
設定時の除外 / 実行時の除外
設定時は起動・再読み込み時に一度だけ適用されルールを削除する(`SecRuleRemoveById` など)。実行時はトランザクションごとに適用されルールを変更する(`ctl:ruleRemoveById` など)。置き場所は前者がCRSの後、後者がCRSの前。
detection paranoia level
`tx.detection_paranoia_level`。ここまでのレベルのルールを実行するが、スコアに影響するのは `tx.blocking_paranoia_level` までに限られる。既定では両者が同値で、レベルを安全に上げる足場になる。
早期ブロック (early blocking)
フェーズ1と3の終わりにも判定を挟む設定。既定は無効。有効にすると判定は4回になるが、公式のアノマリスコアリングの文書は「潜在的なアラートを隠しうる」という欠点を挙げている。