OWASP CRS のルールセットを載せた ModSecurity は十分で有効になり、三日後に切られます——管理領域で記事が保存できなくなり、ファイルのアップロードが壊れ、住所にアポストロフィが入っていたせいで顧客が注文できなくなった後に。「WAF が仕事の邪魔をする」という結論はひとりでに浮かんできますが、それは誤りです。それらの遮断はほぼすべて、三つか四つの正確な例外で片づきます。そして全体の要は、それらを正しく見つけ出すことにあります。

いきなり遮断を有効にしない

最初の一週間は観察だけです。/etc/modsecurity/modsecurity.conf に:

SecRuleEngine DetectionOnly

このモードでは WAF は遮断したはずのものをすべて記録し、何も遮断しません。実際のトラフィックの一週間——管理領域での自分の作業、画像のアップロード、注文も含めて——が、仮定ではなく本物の誤検知の一覧を与えてくれます。On へ切り替えるのは、その一覧を片づけてからにする意味があります。

CRS の仕組み:一つのルールではなく合計

これが以降すべての鍵です。CRS が単一のルールでリクエストを遮断することはほとんどありません。発火したルールはそれぞれリクエストに異常スコアを加算し、合計が閾値を越えたときに遮断が起こります。だからログには一行ではなく数行が出るのであり、その最後が識別子 949110 のルール——合計を足し上げるものです。

実務上の帰結:例外はスコアを加えたルールに対して作るべきで、949110 に対してではありません。合算ルールを切るとセット全体が切れ、WAF は設定ファイルの一行として残るだけになります。

第二の帰結は paranoia レベルです。既定は 1 であり、それが正しい選択です。レベル 2 と 3 は、普通のサイトでは設計上どうしても誤検知を出すルールを追加するもので、レベル 1 を完全に調整し終えてから有効にする価値があります。

原因のルールを見つける

必要なものはすべて監査ログ(/var/log/modsec_audit.log)と Web サーバーのエラーログにあります。遮断が起きた時刻で探します。

sudo grep -o 'id "[0-9]*"' /var/log/modsec_audit.log | sort | uniq -c | sort -rn | head

これが発火したルールの頻度一覧です。次に特定の識別子について、正確に何が引き金になったのかを見ます。

sudo grep -A5 'id "942100"' /var/log/modsec_audit.log | head -40

必要なのは三つです。ルールの識別子、パラメータ名(ARGS:contentARGS:comment)、そしてリクエストのパス。例外はそれらから組み立てます。

おなじみの容疑者

この顔ぶれはサイトごとに繰り返されます。

  • 942100——SQL インジェクション。長い内容のテキスト欄で発火します。記事本文、商品説明、コメント。ごく普通の文章の中の引用符や括弧、select のような語が検出側には疑わしく見えます;
  • 941100 および 941xxx 系——XSS。ビジュアルエディタとともにやって来ます。欄に HTML タグが入るのは、そもそもその機能の意味そのものです;
  • 920420——許可されていない Content-Type。API とファイルのアップロードを壊します。既定で許可される型の集合は狭く、古いバージョンでは application/json がそこに入ってすらいませんでした;
  • 913100——User-Agent によるスキャナ判定。スキャナと一緒に正当なツールも捕まえます。可用性の監視、自分のスクリプトの中の curl など;
  • 200002、200004——リクエスト本文の解析エラー。これはたいてい攻撃ではなく、本文サイズの上限超過、つまり大きなファイルのアップロードを意味します。

例外を作る三つのやり方

粗さの小さい順に。いずれも自分のファイル(たとえば /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES.conf)に書き、CRS 自身のファイルには書きません。ルールセットは更新され、あなたの編集はそれと一緒に消えるからです。

一つのルールから一つのパラメータを外す。最も精密な選択肢であり、目指すべきものです。

SecRuleUpdateTargetById 942100 "!ARGS:content"

一つのパスでだけルールを切る。特定のページが騒がしいときにちょうど合います。エディタ、インポート、レビューのフォームなど。

SecRule REQUEST_URI "@beginsWith /admin/post" \
    "id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=942100"

ルールを丸ごと切る。最後の手段であり、ほとんど常に原因が見つかっていない印です。

SecRuleRemoveById 942100

一つ目と三つ目の差は大きなものです。一つ目では一つの欄——記事本文——が一つのルールによる SQL インジェクション検査から外れるだけ。二つ目と三つ目ではサイト全体が検査されなくなります。手間の差はおよそ五分です。

編集後は設定を確認し、穏やかに読み直させてください。

sudo apachectl configtest && sudo systemctl reload apache2
sudo nginx -t && sudo systemctl reload nginx

一週間を節約する順序

  1. DetectionOnly で一週間、サイト・管理領域・アップロードを含む通常の作業をしながら。
  2. 監査ログからルールの頻度一覧を取る。上から順に片づける——最初の三つ四つがノイズの九割を占めます。
  3. それぞれについて、どのパラメータでどのページかを突き止める。例外はルール単位ではなくパラメータ単位で作る。
  4. そこで初めて SecRuleEngine On
  5. 月に一度、遮断を覗く。サイトが変われば新しい誤検知が出てくるからです。

たいてい崩れるのはまさにこの最後の項目です。監査ログはギガバイト単位のテキストであり、手で読む人はいません。まとめたパネルの意味は、発火したルールの一覧、遮断されたリクエスト、有効なルールセットが grep で掘り出すのではなく目の前にあることです。それがどう見えるかは下のデモページで。