⚙️
graphql-rubyで複雑なQueryを制限する (DoS対策)
はじめに
先日公開された記事「開発者フレンドリーは“ハッカーにとってもフレンドリー” 「GraphQL」の機能を使った悪用事例と究極の対策」を拝見して、graphql-ruby で複雑な Query を制限して DoS 対策できるか確認してみました。
環境
Ruby, Gem
- ruby 3.2.2
- rails 7.1.1
- graphql 2.1.6
- graphiql-rails 1.9.0
GraphQL スキーマ
"""
Requires that exactly one field must be supplied and that field must not be `null`.
"""
directive @oneOf on INPUT_OBJECT
"""
Exposes a URL that specifies the behavior of this scalar.
"""
directive @specifiedBy(
"""
The URL that specifies the behavior of this scalar.
"""
url: String!
) on SCALAR
type Comment {
content: String!
id: ID!
}
type Mutation {
"""
An example field added by the generator
"""
testField: String!
}
"""
A blog post
"""
type Post {
"""
This post's comments, or null if this post has comments disabled.
"""
comments: [Comment!]
content: String!
id: ID!
title: String!
}
type Query {
"""
Find a post by ID
"""
post(id: ID!): Post
"""
An example field added by the generator
"""
testField: String!
}
実行例
Query
{
post(id: 1) {
id
title
content
comments {
id
content
}
}
}
Result
{
"data": {
"post": {
"id": "1",
"title": "Post 1",
"content": "Content for post 1",
"comments": [
{
"id": "1",
"content": "Comment 1"
},
{
"id": "2",
"content": "Comment 2"
},
{
"id": "3",
"content": "Comment 3"
}
]
}
}
}
複雑な Query を制限 (DoS 対策)
タイムアウト設定
複雑な Query で処理に時間がかかる場合、タイムアウトを設定することによって、その時間を超えたときに中断してエラーを返すことができます。
詳しくはこちらの「Timeout」を参照ください。
コード例
class MySchema < GraphQL::Schema
use GraphQL::Schema::Timeout, max_seconds: 3 # タイムアウトを3秒で設定
end
module Types
class QueryType < Types::BaseObject
def post(id:)
# - 省略 -
sleep 5 # タイムアウトするように、sleepを追加
# - 省略 -
end
end
end
実行例
Query
{
post(id: 1) {
id
title
content
comments {
id
content
}
}
}
Result
{
"data": {
"post": null
},
"errors": [
{
"message": "Timeout on Post.id",
"locations": [
{
"line": 3,
"column": 5
}
],
"path": [
"post",
"id"
]
}
]
}
Complexity 制限
リクエストされた Query を解析して、設定された Complexity を超えたときにエラーを返すことができます。
詳しくはこちらの「Prevent complex queries」を参照ください。
コード例
class MySchema < GraphQL::Schema
max_complexity 10 # 最大のComplexityを10で設定
end
実行例
Query
{
post1: post(id: 1) {
id
title
content
comments {
id
content
}
}
post2: post(id: 1) {
id
title
content
comments {
id
content
}
}
post3: post(id: 1) {
id
title
content
comments {
id
content
}
}
}
Result
{
"errors": [
{
"message": "Query has complexity of 21, which exceeds max complexity of 10"
}
]
}
Depth 制限
リクエストされた Query を解析して、設定された Depth を超えたときにエラーを返すことができます。
詳しくはこちらの「Prevent deeply-nested queries」を参照ください。
コード例
class MySchema < GraphQL::Schema
max_depth 2 # 最大のComplexityを2で設定
end
実行例
Query
{
post(id: 1) {
id
title
content
comments {
id
content
}
}
}
Result
{
"errors": [
{
"message": "Query has depth of 3, which exceeds max depth of 2"
}
]
}
独自の Analyzer で制限
上記のようにタイムアウトや Complexity, Depth で制限できますが、独自な制限を行いたい場合があります。
その場合、独自の Analyzer を用意して、そちらで制限することが可能です。
詳しくはこちらの「Ahead-of-Time AST Analysis」を参照ください。
例として、Query の条件数で制限するものを用意していました。
コード例
class MaxQueryCountAnalyzer < GraphQL::Analysis::AST::Analyzer
def initialize(query)
@query_count = 0
super
end
def on_enter_field(node, parent, visitor)
@query_count += 1 if parent.is_a?(GraphQL::Language::Nodes::OperationDefinition)
end
def result
if @query_count > 1
GraphQL::AnalysisError.new("Query count of #{@query_count}, which exceeds max count of 1")
else
nil
end
end
end
class MySchema < GraphQL::Schema
query_analyzer(MaxQueryCountAnalyzer)
end
実行例
Query
{
post1: post(id: 1) {
id
title
content
comments {
id
content
}
}
post2: post(id: 1) {
id
title
content
comments {
id
content
}
}
post3: post(id: 1) {
id
title
content
comments {
id
content
}
}
}
Result
{
"errors": [
{
"message": "Query count of 3, which exceeds max count of 1"
}
]
}
まとめ
GraphQL の Query は柔軟に記載できるので、悪意ある Query を受け取る場合もあります。
悪意ある Query を受け取っても問題ないように、タイムアウトや Complexity, Depth で制限する設定を追加してみてはいかがでしょうか?
Discussion