目次
WAF のルールの基本構造
AWS WAF は HTTPリクエストに対して、優先順位順に条件を評価するルールエンジン です。
JSON を利用して条件分岐を書きますが、そのものというより、ルールの構造をAWSへ渡すための表現形式です。TerraformならHCL、CloudFormationならYAMLでも同じロジックを定義できます。
ちなみに GUI でもある程度は条件分岐ができますが、階層が浅いです。
WAF ルールの基本構造
WAF のルールは、プログラムに置き換えると、おおよそ次のように考えられます。
if 条件A:
ラベルを付ける
Countする
if ラベルAが付いている and 除外条件に該当しない:
Blockする
どのルールでも処理が確定しなければ:
Web ACLのDefaultActionを適用する
実際のWAFは、次の順番で動きます。
HTTPリクエスト
↓
Priorityの小さいルールから評価
↓
Statementで条件判定
↓
一致したらラベル追加やAction実行
↓
終了アクションなら評価終了
↓
終了しなければ次のルールへ
↓
最後まで到達したらDefaultAction
WAFはPriorityの数値が小さい(0→1→2→3)ルールから評価します。BlockやAllowなどの終了アクションが実行されると、その時点でWeb ACLの評価が終了します。一方、Countは基本的に評価を継続するため、後続ルールと組み合わせられます。
JSON部分は「条件式」
たとえば、次のようなWAFのJSONは、
{
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
},
{
"NotStatement": {
"Statement": {
"ByteMatchStatement": {
"SearchString": "/health",
"FieldToMatch": {
"UriPath": {}
},
"PositionalConstraint": "EXACTLY",
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
}
}
}
]
}
}
プログラム風に表すと、
if SQLi_URIPathラベルが付いている
and URIが「/health」ではない:
条件一致
というロジックです。
主に次の論理条件を組み合わせます。
AndStatement → A かつ B
OrStatement → A または B
NotStatement → A ではない
その内側に具体的な検査条件を入れます。
ByteMatchStatement 文字列一致
RegexMatchStatement 正規表現一致
IPSetReferenceStatement IPアドレス一致
GeoMatchStatement 国・地域一致
SqliMatchStatement SQLi検知
XssMatchStatement XSS検知
LabelMatchStatement ラベル一致
RateBasedStatement リクエスト数超過
つまり、WAFを理解する中心は確かに、Statementを使った条件式の組み立てです。
Statement とは
AWS WAFのStatementは、何を条件としてリクエストを判定するか を表す部分です。
上でも記載しましたが、次のように Statement にはさまざまな種類があります。
ByteMatchStatement 文字列一致
RegexMatchStatement 正規表現一致
IPSetReferenceStatement IPアドレス一致
GeoMatchStatement 国・地域一致
SqliMatchStatement SQLi検知
XssMatchStatement XSS検知
LabelMatchStatement ラベル一致
RateBasedStatement リクエスト数超過
つまり、LabelMatchStatementも多数ある条件判定方式の一つです。
AWS WAFの Match Statement は、リクエストやその送信元を、指定した条件と比較するためのものです。
LabelMatchStatementとは
LabelMatchStatementは、そのリクエストに、前のルールによって特定のラベルが付けられているかを確認する Statement です。
ここが重要ですが、LabelMatchStatement自体がSQLインジェクションを検査しているわけではありません。
たとえば次のように、役割が分かれています。
① AWS Managed Rules
リクエストのURIを検査する
② SQLiらしい内容を検知する
SQLi_URIPathラベルを付ける
③ 後続のLabelMatchStatement
SQLi_URIPathラベルがあるか確認する
④ 条件に一致したらBlockする
したがって、LabelMatchStatementは、攻撃を直接検査するものではなく、前の検査結果を参照するものです。
LabelMatchStatementは「Web ACL内ですでに実行されたルールが、リクエストに追加したラベルと照合するStatement」と定義されています。
リクエストには(一時的な)ラベル一覧がある
AWS WAF で評価中のリクエストには、イメージとして次のようなラベル一覧が付いていると考えると分かりやすいです。
{
"request": {
"uri": "/search/1%20OR%201=1",
"labels": [
{
"name": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
]
}
}
ちなみに 1%20OR%201=1は、URL エンコードされた文字列で、デコードすると次のようになります。
1 OR 1=1
%20 は半角スペースを表します。
実際のWAFログでも、次のようなlabelsフィールドとして記録されます。
"labels": [
{
"name": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
]
この一覧に目的のラベルがあるかを確認するのがLabelMatchStatementです。
リクエスト.labels
↓
指定したラベルがあるか検索
↓
あれば true
なければ false
ラベルは Web ACL の評価中だけリクエストに保持され、後続ルールから参照できます。Web ACL の評価が終了すると、リクエストに付いた状態では残りませんが、WAF ログやメトリクスには記録されます。
Key とは何か
Keyは、どのラベル、またはどのラベル名前空間を探すのかを指定する文字列(検索する文字列)です。
今回の例では、次の部分です。
"Key": "awswaf:managed:aws:sql-database:SQLi_URIPath"
つまり、
探す対象
= awswaf:managed:aws:sql-database:SQLi_URIPath
です。
一般的な JSON のKeyという言葉と混同しやすいのですが、ここでのKeyは JSON 用語ではありません。
{
"Key": "検索したいラベル"
}
という、AWS WAF のLabelMatchStatementに定義された正式な設定項目です。
AWS WAF API では、Keyは「照合対象となる文字列(検索する文字列)」と定義され、必須項目です。ラベルは大文字と小文字を区別し、各構成要素は:で区切ります。
次の設定を例にします。
{
"Statement": {
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
},
"Action": {
"Block": {}
}
}
このJSON全体を日本語にすると、リクエストにawswaf:managed:aws:sql-database:SQLi_URIPathというラベルが付いていたら、ブロックする、という意味です。
構造は次のようになっています。
ルール
├─ Statement
│ └─ LabelMatchStatement
│ ├─ Scope
│ └─ Key
│
└─ Action
└─ Block
それぞれの役割は次のとおりです。
| 項目 | 役割 |
|---|---|
| Statement | どんな条件を判定するか |
| LabelMatchStatement | ラベルが付いているかを判定する |
| Scope | どの範囲でラベルを照合するか |
| Key | 実際に探す(照合する)ラベル文字列 |
| Action | 条件に一致した後、何をするか |
リクエストに次のラベルが付いているとします。
"labels": [
{
"name": "awswaf:managed:aws:sql-database:SQLi_URIPath"
},
{
"name": "awswaf:managed:aws:core-rule-set:SizeRestrictions_Body"
}
]
ここで次のLabelMatchStatementを実行します。
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
}
WAFが行うことは、イメージとしては次の処理です。
リクエストのlabelsを確認する
探す文字列:
awswaf:managed:aws:sql-database:SQLi_URIPath
同じラベルがあるか?
↓
ある
↓
LabelMatchStatementは一致
プログラム風に書くと、次のようなものです。
if request.labels contains
"awswaf:managed:aws:sql-database:SQLi_URIPath":
true
else:
false
LabelMatchStatementのKeyは、リクエストに付けられたラベルまたは名前空間と照合する文字列(検索する文字列)です。
Scope と Key の関係
Keyの意味は、Scopeによって変わります。
{
"Scope": "LABEL",
"Key": "..."
}
のように、この二つはセットです。
Scope が LABEL の場合
{
"Scope": "LABEL",
"Key": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
これは、「Keyで指定した特定のラベルを探す」という意味です。
探すラベル名
SQLi_URIPath
まで指定する必要があります。
つまり、次のラベルには一致します。
awswaf:managed:aws:sql-database:SQLi_URIPath
一方、次の別ラベルには一致しません。
awswaf:managed:aws:sql-database:SQLi_Body
awswaf:managed:aws:sql-database:SQLi_QueryArguments
awswaf:managed:aws:sql-database:SQLiExtendedPatterns_URIPath
プログラム風にすると、
request.labelsの中に
指定したラベルが存在するか
です。
Scope が NAMESPACE の場合
{
"Scope": "NAMESPACE",
"Key": "awswaf:managed:aws:sql-database:"
}
これは、「sql-databaseの名前空間に属するラベルをまとめて探す」という意味です。
たとえば、次のようなラベルが対象になります。
awswaf:managed:aws:sql-database:SQLi_URIPath
awswaf:managed:aws:sql-database:SQLi_Body
awswaf:managed:aws:sql-database:SQLi_QueryArguments
プログラム風に表すと、
if any request.label starts with
"awswaf:managed:aws:sql-database:":
条件一致
です。
LABELの場合はラベル名を含める必要があり、NAMESPACEの場合は名前空間部分を指定します。
LABEL と NAMESPACE の違い
まとめると、次の違いです。
Scope = LABEL
特定のラベルを1つ指定する
Scope = NAMESPACE
特定のグループに属するラベルをまとめて指定する
例えるなら、ファイル検索に近いです。
LABEL
= /documents/security/sqli-uri.txt
特定のファイルを指定
NAMESPACE
= /documents/security/
フォルダ配下をまとめて指定
今回の設定は、
{
"Scope": "LABEL",
"Key": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
なので、SQL Database系のラベルなら何でもよい ではなく、SQLi_URIPathという特定のラベルがある場合だけ一致 という設定です。
Statement の構造について
次の一文で全体像をつかむと分かりやすいです。
AndStatementは複数の条件をANDでまとめる箱、Statementsはその箱に入れる条件の一覧、LabelMatchStatementは一覧に入る具体的な条件の一つです。
基本のJSON
{
"Statement": {
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-A"
}
},
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-B"
}
}
]
}
}
}
この条件を日本語にすると、「リクエストにlabel-Aが付いており、さらにlabel-Bも付いている場合に一致する」となります。
論理式では、
label-Aがある AND label-Bがある
です。
一番外側の Statement
{
"Statement": {
...
}
}
この単数形のStatementは、「このルール全体として、どの条件を評価するか」を指定する場所です。
AWS WAF のルールは、大まかに次の構造です。
{
"Name": "ルール名",
"Priority": 10,
"Statement": {
"ここに条件を書く"
},
"Action": {
"Block": {}
}
}
役割を分けると、
Name
ルールの名前
Priority
評価する順番
Statement
どんな条件に一致させるか
Action
一致した場合に何をするか
です。
Statementは「Web リクエストがルールに一致するかを判断するための処理内容」です。Statementの中には、AndStatement、LabelMatchStatement、ByteMatchStatementなど、さまざまな種類の条件を設定できます。
AndStatement
{
"AndStatement": {
...
}
}
AndStatementは、「複数の条件が、すべて一致した場合だけ一致とする論理条件」です。
たとえば、
条件A:SQLiラベルがある
条件B:接続元が許可IPではない
条件C:URIが/adminで始まる
をAndStatementに入れると、
条件A AND 条件B AND 条件C
となります。
3つすべてがtrueになった場合だけ、AndStatement全体がtrueになります。
| 条件A | 条件B | 判定 |
|---|---|---|
| true | true | true |
| true | false | false |
| false | true | false |
| false | false | false |
AWS WAFのAndStatementは、複数のネストされた Statement を AND で結合するもので、少なくとも2つの条件が必要(最低2個の Statement を書かなければならない)です。
Statements
{
"AndStatement": {
"Statements": [
...
]
}
}
複数形のStatementsは、「ANDで結合したい条件を並べる一覧」です。
AWS の API 仕様上、Statementsは「Statementオブジェクトの配列」です。
つまり、次の構造です。
AndStatement
└── Statements
├── 1つ目のStatement
├── 2つ目のStatement
└── 3つ目のStatement
JSONでは、複数の項目を並べるために角括弧[]を使っています。
"Statements": [
条件1,
条件2,
条件3
]
ここで重要なのは、Statementsの中には、単なる文字列ではなく、それぞれ独立した Statement オブジェクトが入ることです。
単数形と複数形の違いは、一番混乱しやすいところです。
単数形のStatement
"Statement": {
...
}
意味は、「一つの条件を置く場所」です。
ただし、その一つの条件としてAndStatementを置くことができます。
"Statement": {
"AndStatement": {
...
}
}
つまり、一番外側から見ると、「このルールの条件は「AND条件」です」ということです。
複数形のStatements
"Statements": [
...
]
こちらは、AndStatementの中で AND 結合する複数の条件 です。
関係を図にすると、
Statement ← ルール全体の条件は1つ
└── AndStatement ← その条件の種類はAND
└── Statements ← ANDで結合する条件の一覧
├── Statement 1
├── Statement 2
└── Statement 3
単数形のStatementの中に、複数形のStatementsが登場するのは、一つのAND条件が、その内部に複数の小さな条件を持つからです。
OrStatement の中にある場合
ちなみに OrStatement の場合は以下のようになります。
{
"OrStatement": {
"Statements": [
{
"条件A": {}
},
{
"条件B": {}
}
]
}
}
この場合は、親がOrStatementなので、
条件A OR 条件B
として評価されます。
どちらか一つでもtrueなら、全体がtrueです。
Statements 自体は AND や OR を決めない
ここが重要です。
"Statements": [
条件A,
条件B
]
このStatements自体には、
- ANDで評価する
- ORで評価する
という意味はありません。
評価方法を決めるのは、外側の親です。
AndStatement
└─ Statements
→ 中の条件をANDで結合
OrStatement
└─ Statements
→ 中の条件をORで結合
つまり、
Statements
=複数の条件を格納する一覧
AndStatement / OrStatement
=その一覧をどの論理で評価するか
という関係です。
NotStatement は単数形
NotStatementは、一つの条件を反転するため、複数形のStatementsではなく、単数形のStatementを使います。
{
"NotStatement": {
"Statement": {
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "trusted-request"
}
}
}
}
これは、
trusted-request ラベルがある
という条件を反転して、
trusted-request ラベルがない
という意味になります。
関係を整理すると次のとおりです。
| 論理条件 | 内部の項目 | 意味 |
|---|---|---|
| AndStatement | Statements | 複数条件をすべて満たす |
| OrStatement | Statements | 複数条件のどれかを満たす |
| NotStatement | Statements | 一つの条件を反転する |
LabelMatchStatement
以下のような LabelMatchStatement があるとします。
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-A"
}
}
LabelMatchStatementは、リクエストに指定したラベルが付いているかを確認する具体的な条件です。
これは AND や OR のような「条件をまとめる箱」ではなく、実際に判定する末端の条件です。
AndStatement
条件をまとめる
LabelMatchStatement
実際にラベルを確認する
LabelMatchStatementは、それより前に評価されたルールによってリクエストに追加済みのラベルを検査します。
たとえば、
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
}
は、
リクエストに
awswaf:managed:aws:sql-database:SQLi_URIPath
というラベルが付いているか?
を判定します。
3つの Statement の関係
Statement の構造を日本語に置き換えると、次のようになります。
{
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-A"
}
},
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-B"
}
}
]
}
}
各部分の意味は、
AndStatement
以下の条件をすべて満たすか確認する
Statements
確認する条件は次の一覧
1つ目のLabelMatchStatement
label-Aがあるか確認する
2つ目のLabelMatchStatement
label-Bがあるか確認する
となります。
処理のイメージは次のとおりです。
リクエストのラベル一覧を確認
↓
label-Aがあるか?
↓
true
label-Bがあるか?
↓
true
両方ともtrueか?
↓
AndStatement全体がtrue
データ構造として見る
プログラムの型のように表すと、次の構造です。
Rule
└── Statement: Statement型
Statement型には、複数の種類があります。
Statement
├── AndStatement
├── OrStatement
├── NotStatement
├── LabelMatchStatement
├── ByteMatchStatement
├── IPSetReferenceStatement
├── SqliMatchStatement
└── その他
そしてAndStatementは、内部に複数のStatementを持ちます。
AndStatement
└── Statements: Statement[]
Statement[]の[]は、
Statementの配列
という意味です。
さらにLabelMatchStatementは、次の設定を持ちます。
LabelMatchStatement
├── Scope
└── Key
全体を型のように書くと、
Rule
└── Statement
└── AndStatement
└── Statements[]
├── Statement
│ └── LabelMatchStatement
│ ├── Scope
│ └── Key
│
└── Statement
└── LabelMatchStatement
├── Scope
└── Key
このように、Statementの中に再びStatementを入れられる構造を、一般にネストと呼びます。AWS WAFの論理Statementはほかの論理Statement内にもネストできますが、コンソールのビジュアルエディタには深いネストの制限があり、複雑な構造はJSONやAPIで設定します。
JSON記号の意味
次の部分を見てみます。
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-A"
}
},
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-B"
}
}
]
波括弧 { }
{
"LabelMatchStatement": {
...
}
}
{}は、一つのオブジェクトを表します。
ここでは、
1つのStatementオブジェクト
です。
角括弧 [ ]
"Statements": [
...
]
[]は、複数の要素を並べる配列です。
ここでは、
複数のStatementを並べる配列
です。
カンマ ,
{
"LabelMatchStatement": {
...
}
},
{
"LabelMatchStatement": {
...
}
}
カンマは、1つ目の条件と2つ目の条件を区切っています。
ただし、このカンマ自体がANDを意味するわけではありません。
カンマ
JSONの項目を区切る記号
AndStatement
条件をANDとして評価する指示
です。
実際の判定例
リクエストに次のラベルが付いているとします。
"labels": [
{
"name": "label-A"
},
{
"name": "label-B"
}
]
WAFの条件が次の場合、
{
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-A"
}
},
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-B"
}
}
]
}
}
判定は次のようになります。
1つ目のLabelMatchStatement
label-Aはあるか?
→ true
2つ目のLabelMatchStatement
label-Bはあるか?
→ true
AndStatement
すべてtrueか?
→ true
一方、リクエストにlabel-Aしか付いていない場合は、
label-Aはあるか?
→ true
label-Bはあるか?
→ false
true AND false
→ false
となります。
異なる種類の Statement も並べられる
Statementsの中をすべてLabelMatchStatementにする必要はありません。
たとえば、
{
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "awswaf:managed:aws:sql-database:SQLi_URIPath"
}
},
{
"ByteMatchStatement": {
"SearchString": "/admin",
"FieldToMatch": {
"UriPath": {}
},
"PositionalConstraint": "STARTS_WITH",
"TextTransformations": [
{
"Priority": 0,
"Type": "NONE"
}
]
}
}
]
}
}
この場合は、
条件1
SQLi_URIPathラベルがある
AND
条件2
URIが/adminで始まる
という意味です。
つまり、
Statements
├── LabelMatchStatement
└── ByteMatchStatement
のように、異なる種類の条件を並べられます。
NotStatement を入れる場合
たとえば、「SQLi ラベルがあり、かつ許可ラベルが付いていない」という条件は、次のように表現できます。
{
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "suspicious:sqli"
}
},
{
"NotStatement": {
"Statement": {
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "trusted:request"
}
}
}
}
]
}
}
論理式では、
suspicious:sqliがある
AND
trusted:requestがない
です。
ここでは、NotStatementの内部は単数形のStatementになっています。
AndStatement
複数の条件が必要
→ Statements[]
NotStatement
反転する条件は1つ
→ Statement
この違いも重要です。
"AndStatement": {
"Statements": [
条件1,
条件2
]
}
"NotStatement": {
"Statement": {
条件1
}
}
ANDは複数条件を結合するため複数形、NOTは一つの条件を反転するため単数形です。
Action はどこに置くのか
LabelMatchStatementごとにBlockやCountを書くわけではありません。
個々のStatement
trueかfalseかを返す
AndStatement
個々の結果をANDでまとめる
RuleのAction
最終結果がtrueなら実行する
正しい配置は次のようになります。
{
"Name": "Block-Suspicious-Request",
"Priority": 10,
"Statement": {
"AndStatement": {
"Statements": [
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-A"
}
},
{
"LabelMatchStatement": {
"Scope": "LABEL",
"Key": "label-B"
}
}
]
}
},
"Action": {
"Block": {}
}
}
処理は、
label-Aがあるか?
↓
label-Bがあるか?
↓
両方あるか?
↓
ある場合、ルール全体が一致
↓
ActionのBlockを実行
となります。
Statement 構造まとめ
Statement
=ルール全体の条件を置く場所
AndStatement
=複数の条件をすべて満たすか判定する論理的な箱
Statements
=AndStatementの中で評価する条件の一覧
LabelMatchStatement
=指定したラベルがリクエストに付いているか確認する具体的な条件
構造は次のように覚えると分かりやすいです。
ルール
└── Statement
└── AndStatement
└── Statements
├── LabelMatchStatement
└── LabelMatchStatement
日本語にすると、
このルールの条件はAND条件です。ANDで確認する条件の一覧は、ラベルAの確認とラベルBの確認です。両方に一致したら、ルールのActionを実行します。