kubell Creator's Note

株式会社kubellのエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

業務中に見つけたセキュリティリスクを OSS に還元した話

はじめに

こんにちは、kubell でバックエンドエンジニアをしている tomoikey です。

突然ですが、GraphQL サーバーを触っていてこんなエラーメッセージを見たことはありませんか?

{
  "errors": [
    {
      "message": "Cannot query field \"usre\" on type \"Query\". Did you mean \"user\"?"
    }
  ]
}

開発中は「おっ、タイポ教えてくれるじゃん!便利!」となるこの機能。でも、これがプロダクション環境で動いているとセキュリティ上かなりマズいことになります。

今回は、この問題に気づいてから Go の GraphQL パーサーライブラリ gqlparsergqlgen にコントリビュートするまでの話をしていきます。

何がマズいのか

GraphQL のサジェスト機能は、存在するフィールド名や型名を推測して「もしかしてこれ?」と教えてくれます。開発者にとっては親切な機能ですが、攻撃者にとってはスキーマの情報を抜き出すための手がかりになってしまいます。

例えば、攻撃者が適当なフィールド名を入力してみるとします。

query {
  ad
}

すると、こんなレスポンスが返ってくるかもしれません。

{
  "errors": [
    {
      "message": "Cannot query field \"ad\" on type \"Query\". Did you mean \"admin\", \"adminSettings\", \"adminUsers\"?"
    }
  ]
}

おっと、admin 系のフィールドが存在することがバレてしまいました。Introspection を無効化していても、この方法でスキーマの一部を推測されてしまう可能性があるわけです。

Rust の GraphQL ライブラリ async-graphql では既にサジェスト機能を無効化するオプションが提供されていましたが、Go の gqlparser にはその機能がありませんでした。

gqlparser への PR

というわけで、PR を出すことにしました。

https://github.com/vektah/gqlparser/pull/319

最初のアプローチ

最初に考えたのは、LoadQueryValidate といった既存の関数に可変長引数でオプションを渡せるようにするアプローチでした。

// こんな感じで使えるようにしたかった
gqlparser.LoadQuery(schema, str)
gqlparser.LoadQuery(schema, str, validator.DisableSuggestion{}) // サジェスト無効化

PR を出した後、別の開発者から「後方互換性を壊さない方法はないか?」というコメントをもらいました。確かに、内部で使っている ruleFunc の型定義も変更する必要があり、既存のコードに影響が出てしまう可能性がありました。

さらに、メンテナーからもレビューをもらいました。

Hey, thanks for working on this. There is some tension between avoiding breaking backwards compatibility and adding more configurable behavior. I don't think this is entirely free from breaking changes. Some recent changes ( #320 ) allow people to reset the rules, so that might be an alternative method that would avoid breaking backwards compatibility.

「後方互換性を完全には保てていないと思う。最近の変更(#320)でルールをリセットできるようになったから、それを使えば後方互換性を保てるかも」という提案でした。

方針転換

メンテナーの提案を受けて、#320 で追加されたルールの追加・置換機能を活用する方向に方針転換しました。

最終的な実装では、既存のバリデーションルールに対応する「サジェストなし版」のルールを追加しました。内部のロジックは共通化しつつ、サジェストを出すかどうかのフラグだけを変えています。

FieldsOnCorrectTypeKnownArgumentNamesKnownTypeNamesValuesOfCorrectType の4つのルールに対して、それぞれ WithoutSuggestions 版を用意しました。

gqlgen への PR

gqlparser の PR がマージされた後、次は gqlgen 側にも対応が必要でした。gqlgen は gqlparser をラップして使っているため、gqlparser の新機能を利用するための設定オプションを追加する必要があります。

https://github.com/99designs/gqlgen/pull/3411

こちらの PR では、gqlgen の Server に SetDisableSuggestion メソッドを追加しました。使い方はシンプルで、サーバー初期化時に呼び出すだけです。

srv := handler.NewDefaultServer(generated.NewExecutableSchema(cfg))
srv.SetDisableSuggestion(true)

これにより、gqlgen を使っている開発者は簡単にサジェスト機能を無効化できるようになりました。

後方互換性について

OSS へのコントリビュートで特に気をつけたいのが後方互換性です。今回のケースでは、最初の実装方針だと既存のコードに影響が出る可能性がありました。

レビューでメンテナーから #320 の機能を活用する方法を提案してもらい、方針を変えることができました。こうしたやり取りを通じてより良い設計にたどり着けるのが OSS の醍醐味でもあります。

breaking change は軽々しくできないし、するべきでもありません。今回は新しいルールを追加する形にしたことで、既存のユーザーには一切影響を与えずに新機能を提供できました。

まとめ

業務中に「これマズくない?」と気づいた問題を OSS に還元できたのは良い経験でした。

GraphQL を本番環境で運用している方は、サジェスト機能の無効化を検討してみてください。Introspection を無効化しているだけでは十分ではない場合があります。

また、OSS へのコントリビュートに興味がある方は、日常の開発で「この機能欲しいな」「これ問題じゃない?」と感じたことをきっかけにしてみるのも良いかもしれません。

参考リンク

最後に

kubell ではバックエンドエンジニアを積極的に募集しています。OSS 活動に興味がある方、GraphQL を使った開発に興味がある方、ぜひ一緒に働きましょう! www.kubell.com