2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【GraphQL】クエリで取得するデータの制限をする

Posted at

はじめに

GraphQLクライアントからクエリを呼び出す際、取得するデータに制限がない場合

意図しない膨大なデータ量のリクエストや、悪意のある攻撃を受けてしまう危険性があります。

そういった事態を防ぐために、GraphQLサーバー側で対策をすることができます。

環境

  • GraphQL Server
    • Node.js
    • Express
    • MongoDB
    • Apollo Server
  • GraphQL Client
    • React.js
    • Apollo Client

データの制限

下記のようなスキーマ・クエリを想定します。

server/typeDefs.graphql
type Photo {
    id: ID!
    url: String!
    name: String!
    postedBy: User!
    taggedUsers: [User!]!
    created: DateTime!
}

type User {
    name: String
    avatar: String
    postedPhotos: [Photo!]!
    inPhotos: [Photo!]!
}
client/query.graphql
query allPhotos {
    allPhotos(first=99999) {
        name
        url
        postedBy {
            name
            avatar
        }
    }
} 

クエリの引数にfirstを追加すると、一つのデータページが返却するデータ件数を指定することができます。
上記のクエリでは、first99999を指定しているため、99999のデータ取得が発生してしまいます。

リゾルバに制限を加える

クエリリゾルバに引数のチェック処理を加えることで、データサイズを制限します。

server/resolver.js
allPhots: (root, data, context) {
    if (data.first > 100) {
        throw new Erroe('Only 100 photos can be requested at a time')
    }
}

クエリ深さの制限

GraphQLがクエリに提供する利点の一つとして、関係するデータを一度に問い合わせられることです。
例えば、下記のようなPhoto APIがあったとします。

client/query.graphql
query getPhoto($id: ID!) {
    Photo(id: $id) {
        name
        url
        postedBy {
            name
            avatar
            postedPhotos {
                name
                url
            }
        }
    }
} 

このクエリは、postedBypostedPhotoという2つの関連するフィールドを問い合わせているので、深さは3であると言えます。

client/query.graphql
query getPhoto($id: ID!) {
    Photo(id: $id) {
        name # 深さ:1
        url  # 深さ:1
        postedBy {
            name    # 深さ:2
            avatar  # 深さ:2
            postedPhotos {
                name   # 深さ:3
                url    # 深さ:3
            }
        }
    }
} 

さらに、上記のクエリを活用すると、下記のように、深さがより深くなるクエリも発行できてしまいます。

client/query.graphql
query getPhoto($id: ID!) {
    Photo(id: $id) {
        name # 深さ:1
        url  # 深さ:1
        postedBy {
            name    # 深さ:2
            avatar  # 深さ:2
            postedPhotos {
                name   # 深さ:3
                url    # 深さ:3
                taggedUsers {
                    name    # 深さ:4
                    avatar  # 深さ:4
                    postedPhots {
                        name    # 深さ:5
                        url     # 深さ:5
                    }
                }
            }
        }
    }
} 

クエリの深さが深くなると、サーバーへかかる負荷は指数関数的に増大してしまいます。

こういったクエリを許容しないために、クエリの深さ制限を実現するnpmパッケージもあります。

npm install graphql-depth-limit

上記パッケージをinstallすると、depthLimit関数を私用してGraphQLサーバーの設定にバリデーションルールを追加できます。

server/index.js
const { ApolloServer } = require(`apollo-server-express`);
const depthLimit = require('graphql-depth-limit');
.....

const server = new ApolloServer({
    typeDefs,
    resolvers,
    validationRules: [depthLimit(5)],
    .....
})

クエリの複雑さ制限

次にクエリ複雑度が高い場合のクエリを考えます。
上記のように深さがそこまで深くないクエリであったとしても、問い合わせられるフィールドの両が多いことで負荷が高くなるクエリがあります。

client/query.graphql
query everything($id: ID!) {
    totalUsers  # 深さ:1
    Photo(id: $id) {
        name    # 深さ:1
        url     # 深さ:1
    }
    allUsers {
        id      # 深さ:2
        name    # 深さ:2
        avatar  # 深さ:2
        postedPhotos {
            name    # 深さ:3
            url     # 深さ:3
        }
        inPhotos {
            name    # 深さ:3
            url     # 深さ:3
            taggedUsers {
                id  # 深さ:4
            }
        }
    }
} 

everythingクエリはクエリの深さ制限5以内には収まっていますが、問い合わせられるフィールドの数が多く、非常に高コストな処理になります。

クエリ複雑度の制限の実装をサポートするnpmパッケージもあります。

npm install graphql-validation-complexity

このgraphql-validation-complexityにはクエリの複雑度を決定するためのルールが定められています。
スカラーフィールドはそれぞれ値1が設定され、フィールドがリストなら10倍ずつ値を増やしたものが設定されています。

client/query.graphql
query everything($id: ID!) {
    totalUsers  # 複雑度:1
    Photo(id: $id) {
        name    # 複雑度:1
        url     # 複雑度:1
    }
    allUsers {
        id      # 複雑度:10
        name    # 複雑度:10
        avatar  # 複雑度:10
        postedPhotos {
            name    # 複雑度:100
            url     # 複雑度:100
        }
        inPhotos {
            name    # 複雑度:100
            url     # 複雑度:100
            taggedUsers {
                id  # 複雑度:1000
            }
        }
    }
}       # 総複雑度1433

クエリの複雑度の制限を1000にすると、こういった特殊なクエリの実行を防ぐことができます。

server/index.js
const { createComplexityLimitRule } = require('graphql-validation-complexity')

.....

const server = new ApolloServer({
    typeDefs,
    resolvers,
    validationRules: [
      depthLimit(5),
      createComplexityLimitRule(1000, {
          onCost: cost => console.log('query cost: ', cost)
      })
    ],
    .....
}

この例では、最大複雑度を1000に制限しました。

また、それぞれのクエリの総コストが計算されたときに、その値を引数として呼び出されるonCost関数も実装しています。

先ほどのクエリは最大複雑度が1000を超えているので、この条件下では実行が許可されません。

最後に

GraphQL Server/Clientにそれぞれ設定を入れることで、負荷がかかるクエリの発行を未然に防ぐことができる、ということがわかりました。

膨大なデータの要求をしなくてよいだけでなく、悪意のある攻撃からも一定の効果があると思いますので、実務にも取り入れられるところは積極的に取り入れていたいです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?