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

WAFルールと例外設定

ルール一致から最終actionまでの順序と、例外・閾値を変えた影響範囲を説明します。

説明

例外を追加する場合、どのルールのどの対象を、どの要求だけ除外するかを狭く定めます。例外の順序が遅ければ、先に遮断されて適用されないことがあります。

同じphase 2の中で例外を前後へ置き、qだけを除いた設定と別引数への一致を比較します。後半では二つの検知点を加算し、閾値5と6で最終動作が変わる様子を追います。

ルール一致から最終actionまでの順序と、例外・閾値を変えた影響範囲を説明します。

1限定例外をルールより前へ置く

/echoの要求に限り、id1001の対象からARGS:qを外します。

from sp01 import * reset() print(rules("exclude")条件付きの対象除外と実行順を読む)

ruleRemoveTargetByIdはルール全体を無効にせず、その要求のqだけを外します。ほかの引数や別パスは引き続き検査します。

2例外ルールを前後に配置して同じ要求を比較する

lateは同じ例外を遮断ルールの後へ置いています。

print(request("exclude"除外を先に適用する設定を試す,"/echo?q=demo-block")["status"]) print(request("late"遮断後に例外を置いた設定を試す,"/echo?q=demo-block")["status"]) print(summary())

前に置いた例外は200、後ろは403です。後ろの例外へ到達する前に、id1001のdenyで処理が中断されました。

3例外に含めない引数とパスを試す

q以外のnoteと、/echo以外の/searchを比較します。

print(request("exclude","/echo?note=demo-block除外対象外の引数を試す")["status"]) print(request("exclude","/search?q=demo-block"除外条件外のパスを試す)["status"])

両方403です。必要な範囲だけ除外し、想定外の入口で検査が消えていないことを確認します。なお試料のパス条件はbeginsWithなので、実運用では/echoの接頭辞全体を許す意図も確認します。

4加点と最終遮断を別ルールで行う

score5ではaの一致が3点、bが2点です。最後のルールが合計5以上で遮断します。

print(rules("score5")初期化・加点・閾値の順を読む) print(request("score5","/echo?a=demo"一つの加点条件だけを満たす)["status"]) print(summary())

aだけは3点なので200です。一致id2001は記録されても、最終遮断のid2003はありません。

5二つの一致が合計閾値へ達する

aとbを両方demoにします。

print(request("score5","/echo?a=demo&b=demo3点と2点の条件を両方満たす")["status"]) print(summary()最終遮断ルールと合計値を照合する)

403で、id2001、2002に続いて2003が記録され、score=5です。個々の一致はpassでも、合計を評価する後段で遮断できます。

6閾値だけを6へ上げる

検知条件は同じで最終閾値を変えます。

print(request("score6"同じ入力を閾値6の設定へ送る,"/echo?a=demo&b=demo")["status"]) print(summary())

200で加点ルール二つの一致は残り、interrupted=Falseです。閾値変更は検知を消す操作とは異なりますが、遮断しなくなる要求が増えます。

7正常値を含めた組合せで影響を確認する

a/bの片方・両方・どちらもなしを比較します。

for query in二条件の全4組を比較する ("a=ok&b=ok","a=demo&b=ok","a=ok&b=demo","a=demo&b=demo"): print(query,request("score5","/echo?"+query)["status"],request("score6","/echo?"+query)["status"])

違いが出るのは両方demoの要求です。変更した例外や閾値を、正常系と検知対象の回帰ケースで照合します。

まとめ

例外は対象・入口・適用順を明示します。

一致と加点、最終遮断を分けて読みます。

設定変更後は正常系と検知対象の組合せを再試験します。

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

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

1
from sp01 import * reset() print(rules("exclude"))
SecRuleEngine On
SecRequestBodyAccess On
SecAuditEngine On
SecAuditLog /tmp/audit.log
SecAuditLogFormat JSON
SecAuditLogParts ABFHZ
SecRule REQUEST_URI "@beginsWith /echo" "id:1200,phase:2,pass,nolog,ctl:ruleRemoveTargetById=1001;ARGS:q"
SecRule ARGS "@contains demo-block" "id:1001,phase:2,deny,status:403,log,t:none,msg:'fixed marker',logdata:'%{MATCHED_VAR_NAME}=%{MATCHED_VAR}'"
SecRule REQUEST_URI "@beginsWith /vulnerable" "id:1100,phase:2,deny,status:403,log,chain,msg:'quote in search'"
SecRule ARGS:q "@contains '" "t:none"
2
print(request("exclude","/echo?q=demo-block")["status"]) print(request("late","/echo?q=demo-block")["status"]) print(summary())
200
403
{'uri': '/echo?q=demo-block', 'engine': 'On', 'interrupted': True, 'matches': [(1001, 'ARGS:q=demo-block')]}
3
print(request("exclude","/echo?note=demo-block")["status"]) print(request("exclude","/search?q=demo-block")["status"])
403
403
4
print(rules("score5")) print(request("score5","/echo?a=demo")["status"]) print(summary())
SecRuleEngine On
SecRequestBodyAccess On
SecAuditEngine On
SecAuditLog /tmp/audit.log
SecAuditLogFormat JSON
SecAuditLogParts ABFHZ
SecAction "id:2000,phase:1,pass,nolog,setvar:tx.score=0"
SecRule ARGS:a "@streq demo" "id:2001,phase:2,pass,log,setvar:tx.score=+3,msg:'marker a'"
SecRule ARGS:b "@streq demo" "id:2002,phase:2,pass,log,setvar:tx.score=+2,msg:'marker b'"
SecRule TX:score "@ge 5" "id:2003,phase:2,deny,status:403,log,msg:'threshold',logdata:'score=%{TX.score}'"
200
{'uri': '/echo?a=demo', 'engine': 'On', 'interrupted': False, 'matches': [(2001, '')]}
5
print(request("score5","/echo?a=demo&b=demo")["status"]) print(summary())
403
{'uri': '/echo?a=demo&b=demo', 'engine': 'On', 'interrupted': True, 'matches': [(2001, ''), (2002, ''), (2003, 'score=5')]}
6
print(request("score6","/echo?a=demo&b=demo")["status"]) print(summary())
200
{'uri': '/echo?a=demo&b=demo', 'engine': 'On', 'interrupted': False, 'matches': [(2001, ''), (2002, '')]}
7
for query in ("a=ok&b=ok","a=demo&b=ok","a=ok&b=demo","a=demo&b=demo"): print(query,request("score5","/echo?"+query)["status"],request("score6","/echo?"+query)["status"])
a=ok&b=ok 200 200
a=demo&b=ok 200 200
a=ok&b=demo 200 200
a=demo&b=demo 403 200

演習完了