Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

API で GraphQL の悪意あるクエリ保護を設定する

最終更新 Markdown で表示Agent セットアップ

Cloudflare GraphQL API を使い、GraphQL API の現在の利用状況を把握し、Cloudflare の GraphQL 悪意あるクエリ保護で悪意のあるクエリを記録またはブロックできます。

はじめに

クエリサイズは、クエリ内の終端フィールド(リーフ)の数です。クエリの深さは、リーフが存在する最も深いレベルです。たとえば、このクエリのサイズは 4 (terminalField[1-4] all contribute to this counter) と報告され、深さは 3 (terminalField3 and terminalField4 are at depth level 3) と報告されます。

GraphQL クエリgraphql
{
	terminalField1
	nonTerminalField1(filter: 123) {
		terminalField2
		nonTerminalField2 {
			terminalField3
			terminalField4
		}
	}
}

GraphQL の統計を取得する

Cloudflare GraphQL API の apiGatewayGraphqlQueryAnalyticsGroups ノードを使い、apiGatewayGraphqlQuerySizeapiGatewayGraphqlQueryDepth のディメンションを取得できます。

GraphQL クエリgraphql
query ApiGatewayGraphqlQueryAnalytics(
	$zoneTag: string
	$start: Time
	$end: Time
) {
	viewer {
		zones(filter: { zoneTag: $zoneTag }) {
			apiGatewayGraphqlQueryAnalyticsGroups(
				limit: 100
				orderBy: [
					apiGatewayGraphqlQuerySize_DESC
					apiGatewayGraphqlQueryDepth_DESC
				]
				filter: { datetime_geq: $start, datetime_leq: $end }
			) {
				count
				dimensions {
					apiGatewayGraphqlQuerySize
					apiGatewayGraphqlQueryDepth
				}
			}
		}
	}
}

上のクエリを実行すると、次のレスポンスが返ります。

レスポンスjson
{
	"data": {
		"viewer": {
			"zones": [
				{
					"apiGatewayGraphqlQueryAnalyticsGroups": [
						{
							"count": 10,
							"dimensions": {
								"apiGatewayGraphqlQueryDepth": 1,
								"apiGatewayGraphqlQuerySize": 11
							}
						},
						{
							"count": 10,
							"dimensions": {
								"apiGatewayGraphqlQueryDepth": 1,
								"apiGatewayGraphqlQuerySize": 2
							}
						}
					]
				}
			]
		}
	},
	"errors": null
}

このレスポンス例では、選択した期間に、深さ 1・サイズ 11 のリクエストが 10 件、深さ 1・サイズ 2 のリクエストが 10 件観測されています。

GraphQL の統計を分析する

レスポンスを使い、各属性のパーセンタイルを計算し、許可するしきい値を設定できます。たとえば、クエリサイズや深さに 1.5 * p99 のような単純なヒューリスティックを使えます。

上の GraphQL API レスポンス(JSON ファイル)を入力として、クエリサイズと深さのパーセンタイルを出力する、簡単な Python スクリプトです。

Python スクリプトpython
#!/usr/bin/env python3

import json
import numpy as np
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--response", help="Path to the API JSON response file with the apiGatewayGraphqlQueryAnalyticsGroups node", required=True)
args = parser.parse_args()
with open(args.response) as f:
    query_sizes = np.array([], dtype=np.uint16)
    query_depths = np.array([], dtype=np.uint8)
    data = json.load(f)['data']['viewer']['zones'][0]['apiGatewayGraphqlQueryAnalyticsGroups']
    for datapoint in data:
        query_sizes = np.append(query_sizes, [datapoint['dimensions']['apiGatewayGraphqlQuerySize']] * datapoint['count'])
        query_depths = np.append(query_depths, [datapoint['dimensions']['apiGatewayGraphqlQueryDepth']] * datapoint['count'])

    quantiles = [0.99, 0.95, 0.75, 0.5]
    print('\n'.join([f"Query size {int(q * 100)}th percentile is {v}" for q, v in zip(quantiles, np.quantile(query_sizes, quantiles))]))
    print('\n'.join([f"Query depth {int(q * 100)}th percentile is {v}" for q, v in zip(quantiles, np.quantile(query_depths, quantiles))]))

上のスクリプトを実行すると、次の出力が得られます。

出力例json
./calculator.py --response=response.json
Query size 99th percentile is 11.0
Query size 95th percentile is 11.0
Query size 75th percentile is 11.0
Query size 50th percentile is 6.5
Query depth 99th percentile is 1.0
Query depth 95th percentile is 1.0
Query depth 75th percentile is 1.0
Query depth 50th percentile is 1.0

受信 GraphQL クエリに上限を設定する

API Shield のお客様は、カスタムルールで次の 3 つのフィールドを使えます。

  • cf.api_gateway.graphql.query_size は GraphQL クエリのサイズです。
  • cf.api_gateway.graphql.query_depth は GraphQL クエリの深さです。
  • cf.api_gateway.graphql.parsed_successfully は、Cloudflare がクエリを解析できたかどうかを示します。現在はベストエフォートの解析です。有効なクエリでも解析できないことがあります。そのため、GraphQL セキュリティルールをデプロイするときは、カスタムルールに and cf.api_gateway.graphql.parsed_successfully フィルターを含めてください。

たとえば、深く入れ子になっていて 30 を超えるフィールドを要求するクエリをブロックするには、API またはダッシュボードで次のルールをデプロイできます。

(cf.api_gateway.graphql.query_size > 30 and cf.api_gateway.graphql.query_depth > 7 and cf.api_gateway.graphql.parsed_successfully)

役に立ちましたか?