【AWS】WAF のルールの構造について

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)ルールから評価します。BlockAllowなどの終了アクションが実行されると、その時点で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

LabelMatchStatementKeyは、リクエストに付けられたラベルまたは名前空間と照合する文字列(検索する文字列)です。

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の中には、AndStatementLabelMatchStatementByteMatchStatementなど、さまざまな種類の条件を設定できます。

AndStatement

{
  "AndStatement": {
    ...
  }
}

AndStatementは、「複数の条件が、すべて一致した場合だけ一致とする論理条件」です。

たとえば、

条件A:SQLiラベルがある
条件B:接続元が許可IPではない
条件C:URIが/adminで始まる

AndStatementに入れると、

条件A AND 条件B AND 条件C

となります。

3つすべてがtrueになった場合だけ、AndStatement全体がtrueになります。

条件A条件B判定
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

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 ラベルがない

という意味になります。

関係を整理すると次のとおりです。

論理条件内部の項目意味
AndStatementStatements複数条件をすべて満たす
OrStatementStatements複数条件のどれかを満たす
NotStatementStatements一つの条件を反転する

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ごとにBlockCountを書くわけではありません。

個々の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を実行します。