🚫

【GraphQL】深さ・複雑さ を制限する【Laravel・lighthouse】

2023/10/22に公開

GraphQLは、クライアントが欲しい情報を、クライアント側が指定して取得することができます。

その反面、指定され方によっては、サーバー側に大きな負荷がかかる可能性があります。

例えば以下のスキーマがあった場合、
※設計は今回の事象を説明する為のもの なので適当です。

type Query {
fetchAllPlayers: [Player!]! @all
}

type Player {
    id: ID!
    name: String!
    team: Team!
}

type Team {
    id: ID!
    name: String!
    player: [Player!]! @hasMany
}

以下のようにapiを叩くことが可能です。
非常に無駄な叩き方ですし、サーバー側に負荷をかけてしまう可能性もあります。

query{
  fetchAllPlayers{
    id
    name
    team {
      id
      name
      player {
        id
        name
        team {
          id
          name
          player {
            id
            name
            team {
              id
              player {
                id
                name
                team {
                  id
                  name
                }
              }
            }
          }
        }
      }
    }
  }
}

上記の問題を解決するために、
2つのアプローチを行い、解決することを目指します。

depth (深さ)

クエリの深さの制限をかけることにより、高負荷で複雑なネストのクエリを叩くことを防ぐことができます。

config/lighthouse.phpのmax_query_depthを1に設定します。
デフォルトでは0となっており、max_query_depthの設定は無効化されています。

lighthouse.php
    'security' => [
        'max_query_complexity' => \GraphQL\Validator\Rules\QueryComplexity::DISABLED,
-        'max_query_depth' => \GraphQL\Validator\Rules\QueryDepth::DISABLED,
+        'max_query_depth' => 1,
        'disable_introspection' => (bool) env('LIGHTHOUSE_SECURITY_DISABLE_INTROSPECTION', false)
            ? \GraphQL\Validator\Rules\DisableIntrospection::ENABLED
            : \GraphQL\Validator\Rules\DisableIntrospection::DISABLED,
    ],

上記設定で以下クエリを叩くと

query{
  fetchAllPlayers{
    id
    name
    team {
      id
      player {
        id 
        name
      }
    }
  }
}

エラーがレスポンスされ、クエリを叩く際の「深さ」が制限できていることがわかりました。

{
  "errors": [
    {
      "message": "Max query depth should be 1 but got 2.",
      "extensions": {
        "category": "graphql"
      }
    }
  ]
}

complexity(複雑さ)

クエリの複雑さを計算した値が、制限値を超えないか検証を行う方法になります。

https://webonyx.github.io/graphql-php/security/#query-complexity-analysis
lighthouseでの計算方法は

Every field in the query gets a default score 1 (including ObjectType nodes). Total complexity of the query is the sum of all field scores.

とあり、

例えば以下の複雑さのスコアは8ということになります。

query{
  fetchAllPlayers{ #+1
    id #+1
    name #+1
    team #+1 {
      id #+1
      player #+1 {
        id #+1
        name #+1
      }
    }
  }
}

実際にlighthouse.phpで制限をかけて叩くと、エラーレスポンスを受け取ることができます。

lighthouse.php
    'security' => [
-        'max_query_complexity' => \GraphQL\Validator\Rules\QueryComplexity::DISABLED,
+       'max_query_complexity' => 1,
        'max_query_depth' => \GraphQL\Validator\Rules\QueryDepth::DISABLED,
        'disable_introspection' => (bool) env('LIGHTHOUSE_SECURITY_DISABLE_INTROSPECTION', false)
            ? \GraphQL\Validator\Rules\DisableIntrospection::ENABLED
            : \GraphQL\Validator\Rules\DisableIntrospection::DISABLED,
    ],
{
  "errors": [
    {
      "message": "Max query complexity should be 1 but got 8.",
      "extensions": {
        "category": "graphql"
      }
    }
  ]
}

複雑さ計算のカスタマイズ

またlighthouseでは複雑さ計算のカスタマイズをすることができます。
complexityディレクティブを使います。
https://lighthouse-php.com/master/api-reference/directives.html#complexity

fetchAllPlayers: [Player!]!
        @all
        @complexity(resolver: "App\\GraphQL\\Security\\ComplexityAnalyzer")

childrenComplexityにfetchAllPlayersフィールド配下の計算後の値が入っています。
つまり前回の例だと7 (8-1)。$argsにはクエリ叩く時のvariablesが入っています。

以下では、argsの要素数が0より大きい場合は、childrenComplexityの値を2倍にして、
そうではない場合0を返すカスタムをしました。

ComplexityAnalyzer.php
<?php

namespace App\GraphQL\Security;

final class ComplexityAnalyzer
{
    public function __invoke(int $childrenComplexity, array $args): int
    {
        return count($args) > 0 ? $childrenComplexity * 2 : 0;
    }

}

Discussion