Request Validator PluginでAPIリクエストを事前検証する

本記事は「Kong Advent Calendar 2024」の7日目のエントリとして、Request Validator Pluginについて解説する。

APIセキュリティを考える際にデファクトの1つとして使われるのは[OWASP API Security Top 10](https://owasp.org/API-Security/editions/2023/en/0x00-header/)だが、この中で設計者・運用者が意図しないパスやパラメータへのアクセスについて言及されているものがいくつかある。
これはKong Gatewayを使っている場合、[Route](https://docs.konghq.com/gateway/latest/key-concepts/routes/)を適切かつ厳格に設定していればある程度防げたりもするのだが、更にしっかり防ぎたい人にオススメなのが[Request Validator Plugin](https://docs.konghq.com/hub/kong-inc/request-validator/)である。
このプラグインを使うと、リクエストの本文のスキーマやパラメータの検証を実施し、意図しないリクエストであればアップストリームに転送せずに弾くようなことが出来る。
今回はこのプラグインの使い方や効果について簡単に検証する。

## Request Validatorの設定項目
Pluginの設定画面は以下のようになっている。
![20241120095646.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/92aa8ccf-485e-f12e-36ff-c7009af8f046.png)

各項目の意味を[Configuration reference](https://docs.konghq.com/hub/kong-inc/request-validator/configuration/)から確認する。

|パラメータ|意味|
|---|---|
|Body Schema(`body_schema`)|検証対象がリクエストボディの場合のスキーマの指定。この`body_schema`か後述の`parameter_schema`のどちらかの指定は必須。</br>例）`{"name":{"type": "string", "required": true}}`|
|Content Type Parameter Validation(`content_type_parameter_validation`)|メディアタイプ(Content-Type)を検証するかどうか|
|Parameter Schema(`parameter_schema`)|検証対象がパラメータの場合の各種設定やスキーマの指定|
|- `in`|検証対象がどこにいるか(`query`、`header`、`path`から指定）|
|- `name`|パラメータ名|
|- `required`|ここで設定するパラメータがリクエスト内に必須かどうか|
|- `style`|パラメータをチェックする際のデシリアライズの方法。設定値に関しては後述|
|- `explode`|パラメータの指定の仕方を指定。trueだとkey=value1&key=value2のようにkeyとvalueを1対1の形で渡すことが前提となるが、falseだとkey=value1,value2のようにvalueを文字列でまとめて渡す形になる|
|- `schema`|パラメータのスキーマを指定</br>例）`{ "type": "array", "items": {"type": "string"}}`|
|Verbose Response(`verbose_response`)|エラー時に詳細メッセージを出すかどうか|
|Version(`version`)|バリデータの選択。`kong`(Kong独自のもの)か`draft4`(JSON Schema Draft-4互換)が選択可能|
|Allowed Content Types(`allowed_content_types`)|ボディのスキーマチェック時、許可されるメディアタイプを指定。メディアタイプで許可する値については`charset`を併せて指定する。（例：`application/json; charset=UTF-8`）|


`parameter_schema.style`で指定できるデシリアライズの方法はKongのドキュメントには記載がないが、OpenAPI Sepcificationで[言及されている](https://spec.openapis.org/oas/v3.1.0.html#style-values)のでこれが少し参考になると思う（RFCに飛ばされるものも多いが）。
恐らく以下のような感じになる（未検証）。
- `query`
  - `form`：キーと値のペアとしてエンコード
  - `spaceDelimited`：スペースで区切られた値としてエンコード
  - `pipeDelimited`：パイプ（|）で区切られた値としてエンコード
  - `deepObject`：オブジェクトのネストされた構造を表現するために使用
- `path`
  - `simple`：そのまま利用
  - `label`：ドット（.）で区切られた形式でエンコード
  - `matrix`：セミコロン（;）で区切られた形式でエンコード
- `header`
  - `simple`：そのまま利用

## 検証の前提
以下の環境で確認する。
- Kong Gatewayはlocalhost上に構築済み（Docker）
- 検証用のアップストリームはKong提供のhttpbin([https://httpbin.konghq.com](https://httpbin.konghq.com))を使用し、Proxyの`/httpbin`からアクセス出来るようにする

## 検証
Request Validatorを検証するにあたり、ServiceとRouteを作成する。
(設定内容の説明は割愛)
```sh
curl -s -X POST localhost:8001/services \
  -H "Content-Type: application/json" \
  -d '{
    "name":"httpbin-svc",
    "url":"https://httpbin.konghq.com"
    }'
```
```sh
curl -s -X POST localhost:8001/services/httpbin-svc/routes \
  -H "Content-Type: application/json" \
  -d '{
    "name":"httpbin-rt",
    "paths":["/httpbin"]
    }'
```
作成したRouteに対してRequest Validator Pluginを設定する。
なお、今回設定は全てUIから行ったが、APIやdeckからも設定可能。

#### 検証1： クエリ`tags`に数値のみ指定されていることをチェック
特定のパラメータ(`tags`)に絞って検証するので、`parameter_schema`を使う。
以下で設定する。
- `verbose_response`: `true`
- `parameter_schema.in`: `query`
- `parameter_schema.name`: `tags`
- `parameter_schema.style`: `form`
- `parameter_schema.explode`: `true`
- `parameter_schema.schema`: 以下のスキーマを入力する
```json
{ 
  "type": "array", 
  "items": {
  	  "type": "integer"
  }
}
```
実際に動作させてみる。
クエリとしてtagsに文字列を指定し、他のクエリの影響も確認するため`hoge=fuga`というクエリもセットで渡してみる。
```json
$ curl "localhost:8000/httpbin/anything?tags=aaa&hoge=fuga"
{"message":"query 'tags' validation failed, [error] failed to validate item 1: wrong type: expected integer, got string","data":["aaa"]}
```
数値を期待しているのに文字列が与えられているためエラーとなった。
ちなみに`verbose_response`を指定しない場合は以下となる。
```json
{"message":"request param doesn't conform to schema"}
```
`verbose_response`をつけると攻撃者にヒントを与えることになるので、実運用では外した方がよい。
次に数値を渡してみる。
```json
$ curl "localhost:8000/httpbin/anything?tags=123&hoge=fuga"
{
  "args": {
    "hoge": "fuga",
    "tags": "123"
  },
  "data": "",
```
これだと上手くいった。

#### 検証2： ボディに許可されていないkeyを含む場合にエラーを返す
例えば以下のようなデータを受け取るアップストリームがあるとし、`name`のみ必須で`id`は任意、それ以外のパラメータは受け付けないようにしたいとする。
```json
{
  "name": 文字列,
  "id": 数値
}
```
その場合、以下のような感じで設定する。

- `verbose_response`: `true`
- `content_type_parameter_validation`: `true`
- `version`: `draft4`
- `allowed_content_types`: `application/json`
- `body_schema`: 以下のスキーマ
```json
{
  "type": "object",
  "properties": {
    "id": {
      "type": "integer",
      "minimum": 1
    },
    "name": {
      "type": "string",
      "minLength": 1
    }
  },
  "required": ["name"], 
  "additionalProperties": false
 }
```
`id`と`name`は渡すことが可能で、必須パラメータを`"required": ["name"]`で指定し、`"additionalProperties": false`で他のパラメータを禁止している。

検証してみる。
最初に以下のような通って欲しいリクエストを試してみる。
```json
$ curl "localhost:8000/httpbin/anything" \
   -H "Content-type: application/json" \
   -d '{
   	  "name": "test",
   	  "id": 123
   	}'
```
実行結果は以下となり、正常にリクエストがアップストリームに到達した。
```json
{
  "args": {},
  "data": "{\n     \"name\": \"test\",\n     \"id\": 123\n   }",
：（省略）
  "json": {
    "id": 123,
    "name": "test"
  },
：（省略）
```
次にリクエストをガードして欲しいケースを試してみる。
悪意のあるユーザがパラメータに当たりをつけて`admin: true`なんかを指定したとする。
```json
$ curl "localhost:8000/httpbin/anything" \
   -H "Content-type: application/json" \
   -d '{
   	  "name": "test",
   	  "id": 123,
   	  "admin": true
   	}'
```
結果は以下のようになり、追加のパラメータは禁止されていることが確認できる。
```json
{"message":"additional properties forbidden, found admin"}
```
なお、必須パラメータを指定しなかった場合の結果は以下。
```json
$ curl "localhost:8000/httpbin/anything" \
   -H "Content-type: application/json" \
   -d '{
   	  "id": 123
   	}'
{"message":"property name is required"}
```
こちらも期待通りの結果となった。

## まとめ
ということで、Request Validatorを使えば意図しないリクエストを防いだり、意図しない値をアップストリームに渡すのを防げることが確認できた。
ポイントとしてはガードするためのスキーマを如何に書くか、というところになりそう。
これが適切に用意できないとプラグインがあってもセキュリティは向上できないので、API設計時のスキーマ定義はしっかりやっておいた方がよさそうだ。