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

WAFのリクエスト解析

検査対象へ値が届いたか、どの値が一致したか、どこまで本文を検査できたかを確認します。

説明

WAFはHTTP要求から引数や本文を取り出し、設定したルールへ照合します。URL符号化を何回解くか、本文をどの形式として解析するかによって、ルールに渡る値が変わります。

Corazaの固定試料ルールで、文字列demo-blockを検査します。通常の引数、二重符号化、JSON、引数の欠落、本文の上限を順に変え、WAFのログとアプリ到達を比較します。

検査対象へ値が届いたか、どの値が一致したか、どこまで本文を検査できたかを確認します。

1検査対象とルールを読む

baselineはARGS全体にdemo-blockが含まれると、phase 2で403を返す試料です。CRS全体の検知性能を測る例ではありません。

from sp01 import * reset() print(rules("baseline")現在の試料設定を読む)

SecRuleEngine Onと本文アクセスを有効にしています。id:1001は引数、id:1100の連鎖ルールは/vulnerableの検索値に含まれる引用符を検査します。

2通常引数が一致した位置を調べる

qへ固定マーカーをそのまま入れます。

print(request("baseline"実WAFへHTTP要求を送る,"/echo?q=demo-block")) print(summary()直近監査ログの一致情報を抽出する)

403でアプリ未到達です。監査ログのid1001とARGS:q=demo-blockが、一致したルールと値を示します。

3URL符号化した同じ値を比較する

dだけを%64にします。

print(request("baseline","/echo?q=%64emo-block一度符号化した値を送る")) print(summary())

同じ403とdemo-blockの一致値です。クエリー解析時のURLデコードで%64がdになってから照合されます。

4二重符号化と追加変換を比較する

%自体を%25にした入力を、追加変換のない設定とt:urlDecodeを加えた設定へ送ります。

print(request("baseline","/echo?q=%2564emo-block"一度だけ解釈する設定へ送る)) print(request("decode"追加URLデコード付き設定へ送る,"/echo?q=%2564emo-block")) print(summary())

baselineでは%64emo-blockのままアプリへ到達し、decodeは追加の変換で403になります。変換を増やすことが常に正しいわけではなく、アプリが実際に解釈する回数と揃えます。

5JSONを引数として検査する条件を確認する

JSON本文は専用processorを選んだ設定と比較します。

body = b'{"q":"demo-block"}' print(request("baseline","/echo",body,"application/json"JSON形式の本文を送る)) print(request("json"JSON processorを明示した設定へ送る,"/echo",body,"application/json")) print(summary())

baselineは200、JSON processorを設定した側は403です。本文アクセスがOnというだけで、すべての形式がARGSへ同じ形で展開されるとは限りません。

6引数が存在しない場合を別に検査する

値への一致ルールだけでは、そもそもqがない要求は一致しません。requiredには&ARGS:qの個数検査を追加しています。

print(request("baseline","/echo")["status"]) print(request("required","/echo")存在を必須にした設定へ送る["status"]) print(request("required","/echo?q="空値と欠落を比較する)["status"])

順に200、400、200です。欠落と空文字列は異なります。必須か、空を許すか、形式を制限するかを別々の条件として定めます。

7本文検査の上限とアプリ到達を照合する

limitはSecRequestBodyLimitを16、ActionをRejectに設定しています。Coraza 3.7.0の境界を15・16・17バイトで調べます。

reset() for size in (15,16,17)設定値の直前・一致・超過を比較する: print(size,request("limit","/echo",b"x"*size,"text/plain")["status"]) print(observed()後段の受信量を照合する)

15は200、16と17は413です。この版の実装は設定値に達した時点で拒否します。アプリ記録も15バイトの1件だけです。説明上の「上限以内」という解釈に頼らず、採用する版の境界を実測します。

まとめ

符号化と本文processorによって検査値が変わります。

引数の欠落・空値・不正値を別の条件で扱います。

検査上限と超過時の動作を、アプリ到達まで照合します。

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

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

1
from sp01 import * reset() print(rules("baseline"))
SecRuleEngine On
SecRequestBodyAccess On
SecAuditEngine On
SecAuditLog /tmp/audit.log
SecAuditLogFormat JSON
SecAuditLogParts ABFHZ
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("baseline","/echo?q=demo-block")) print(summary())
{'status': 403, 'origin': False, 'body': ''}
{'uri': '/echo?q=demo-block', 'engine': 'On', 'interrupted': True, 'matches': [(1001, 'ARGS:q=demo-block')]}
3
print(request("baseline","/echo?q=%64emo-block")) print(summary())
{'status': 403, 'origin': False, 'body': ''}
{'uri': '/echo?q=%64emo-block', 'engine': 'On', 'interrupted': True, 'matches': [(1001, 'ARGS:q=demo-block')]}
4
print(request("baseline","/echo?q=%2564emo-block")) print(request("decode","/echo?q=%2564emo-block")) print(summary())
{'status': 200, 'origin': True, 'body': {'args': {'q': ['%64emo-block']}, 'bytes': 0, 'path': '/echo'}}
{'status': 403, 'origin': False, 'body': ''}
{'uri': '/echo?q=%2564emo-block', 'engine': 'On', 'interrupted': True, 'matches': [(1001, 'ARGS:q=demo-block')]}
5
body = b'{"q":"demo-block"}' print(request("baseline","/echo",body,"application/json")) print(request("json","/echo",body,"application/json")) print(summary())
{'status': 200, 'origin': True, 'body': {'args': {}, 'bytes': 18, 'path': '/echo'}}
{'status': 403, 'origin': False, 'body': ''}
{'uri': '/echo', 'engine': 'On', 'interrupted': True, 'matches': [(1001, 'ARGS:json.q=demo-block')]}
6
print(request("baseline","/echo")["status"]) print(request("required","/echo")["status"]) print(request("required","/echo?q=")["status"])
200
400
200
7
reset() for size in (15,16,17): print(size,request("limit","/echo",b"x"*size,"text/plain")["status"]) print(observed())
15 200
16 413
17 413
[{'args': {}, 'bytes': 15, 'method': 'POST', 'path': '/echo'}]

演習完了