一覧SP-01-2 · WAF:HTTPの検査と制御

WAFの検知モードと遮断モード

ログの有無だけで防御成功を判断せず、実際の経路とアプリ到達を確認します。

説明

WAFログにルール一致があることと、アプリへの要求を止めたことは同じではありません。検知だけのモードでは一致を記録しながら要求を通します。またWAFを通らない入口が残れば、その経路では検査されません。

同じ固定要求をOn、DetectionOnly、Off、WAFなしの経路へ送ります。実HTTPのstatus、後段アプリの到達記録、監査ログの中断状態を照合します。

ログの有無だけで防御成功を判断せず、実際の経路とアプリ到達を確認します。

1同じ要求を用意して到達記録を初期化する

各経路は別ポートですが、同じ後段アプリへ接続しています。

from sp01 import * reset() path = "/echo?q=demo-block" print({name:PROFILES[name]モードごとの入口を確認する for name in ("baseline","detection","off","direct")}) print(observed()後段の初期状態を確認する)

到達記録は空です。以後は要求の中身を変えず、通る経路とモードを変えます。

2Onで遮断した結果を確認する

baselineは一致したルールのdenyを実行します。

print(request("baseline",path)遮断モードを通す) print(summary()) print(observed()後段に届いていないことを照合する)

403、interrupted=True、アプリ記録は空です。ルール一致と到達抑止が両方確認できます。

3DetectionOnlyでは一致してもアプリへ届く

同じ要求を検知だけの入口へ送ります。

print(request("detection",path)検知専用モードを通す) print(summary()) print(len(observed())実到達件数を確認する)

200でアプリ記録は1件。id1001は残りますがinterrupted=Falseです。ログのAccess denied相当の有無だけでなく、モードと実応答も読みます。

4Offではルール評価の記録も増えないことを確認する

監査行数を要求前後で比較します。

before = len(audit())要求前の監査件数を記録する print(request("off",path)ルールエンジンを停止した入口を通す) print(len(audit())-before,len(observed()))

200でアプリ到達は2件、監査行の増分は0です。この版と試料設定での挙動として、Offを検知専用と混同しないようにします。

5WAFなしの経路を比較する

directにはWAFハンドラーがなく、後段へ直接中継します。

print(rules("direct")WAFハンドラーがない構成を確認する) print(request("direct",path)検査経路を通らない要求を比較する) print(len(observed()))

アプリ到達は3件です。別の公開入口や内部向け入口がこの状態なら、WAF経路だけを試験しても保護範囲全体を確認したことにはなりません。

6通常要求を遮断モードで通す

qをhelloへ変更します。

print(request("baseline","/echo?q=hello一致しない通常値へ変える")) print(summary())

200でmatchesは空、interrupted=Falseです。遮断モードでも一致しない通常要求は到達します。正常系を合わせて確認します。

7後段が受け取った要求をまとめて照合する

Onで拒否した最初の要求だけが後段に残っていないはずです。

print([(r["path"],r["args"]["q"][0]) for r in observed()後段が実際に受け取った値を一覧にする])

demo-blockが3件、helloが1件です。モードと経路に応じた4件の到達を、HTTP応答とは別の記録で確認しました。

まとめ

一致ログと遮断成功を分けて確認します。

DetectionOnly・Off・WAFなしの違いを到達記録で調べます。

通常要求も含めて保護経路を試験します。

操作履歴と解説を振り返れます。

Python HTTP client / Coraza 3.7.0 / coraza-caddy 2.5.0 / Caddy 2.11.4

1
from sp01 import * reset() path = "/echo?q=demo-block" print({name:PROFILES[name] for name in ("baseline","detection","off","direct")}) print(observed())
{'baseline': 44730, 'detection': 44731, 'off': 44732, 'direct': 44733}
[]
2
print(request("baseline",path)) print(summary()) print(observed())
{'status': 403, 'origin': False, 'body': ''}
{'uri': '/echo?q=demo-block', 'engine': 'On', 'interrupted': True, 'matches': [(1001, 'ARGS:q=demo-block')]}
[]
3
print(request("detection",path)) print(summary()) print(len(observed()))
{'status': 200, 'origin': True, 'body': {'args': {'q': ['demo-block']}, 'bytes': 0, 'path': '/echo'}}
{'uri': '/echo?q=demo-block', 'engine': 'DetectionOnly', 'interrupted': False, 'matches': [(1001, 'ARGS:q=demo-block')]}
1
4
before = len(audit()) print(request("off",path)) print(len(audit())-before,len(observed()))
{'status': 200, 'origin': True, 'body': {'args': {'q': ['demo-block']}, 'bytes': 0, 'path': '/echo'}}
1 2
5
print(rules("direct")) print(request("direct",path)) print(len(observed()))
no WAF handler
{'status': 200, 'origin': True, 'body': {'args': {'q': ['demo-block']}, 'bytes': 0, 'path': '/echo'}}
3
6
print(request("baseline","/echo?q=hello")) print(summary())
{'status': 200, 'origin': True, 'body': {'args': {'q': ['hello']}, 'bytes': 0, 'path': '/echo'}}
{'uri': '/echo?q=hello', 'engine': 'On', 'interrupted': False, 'matches': []}
7
print([(r["path"],r["args"]["q"][0]) for r in observed()])
[('/echo', 'demo-block'), ('/echo', 'demo-block'), ('/echo', 'demo-block'), ('/echo', 'hello')]

演習完了