async-graphql製アプリのセキュリティ対策

概要

logmi.jp

上記を読んで、先日つくったGraphQLアプリに対策を施すことができるか確認した。

先日作ったGraphQLアプリ

vraisamis.hatenadiary.jp

github.com

やったこと

  • introspection対策
  • field suggestion対策
  • 再帰的Fragment対策
  • alias対策

やっていないこと

introspection対策、field suggestion対策

この2つはメソッドを呼ぶだけで対策が可能。

SchemaBuilder 型のメソッドに disable_introspection() disable_suggestions() があるので、それらを呼ぶだけ。

例えば、releaseビルドでのみ無効化したいときは、以下のようにする。

let s: SchemaBuilder<_, _, _> = ...;
if cfg!(not(debug_assertions)) {
    s = s.disable_suggestions().disable_introspection();
}

再帰的Fragment対策

元々対策されている模様。

(……が、エラーメッセージが「再帰深度の最大は32です」なので確認したほうがよさそう)

query {
  user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    ...A
  }
}

fragment A on User {
  name
  ...B
}

fragment B on User {
  ...A
}
{
  "data": null,
  "errors": [
    {
      "message": "The recursion depth of the query cannot be greater than `32`",
      "locations": [
        {
          "line": 12,
          "column": 20
        }
      ]
    }
  ]
}

alias対策

aliasはどれだけ多くても許可するようになっている。以下のように10000個のaliasをリクエストしたが普通にresponseが返ってきた。

{
  alias0: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    name
  }
  alias1: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    name
  }
  # ...
  alias9999: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    name
  }
}

これに対策するには独自のExtensionの実装が必要。以下に記載がある。

async-graphql.github.io

この中の parse_query を利用する。

let result = next.run(ctx, query, variables).await?; とすると、resultの中からパース済みのGraphQL queryを取り出せるので、そこからaliasを見つけてカウントしていき、エラーを出すようにする。

今回は以下のように制限した。

  • aliasは全部で10個まで
  • 各階層(ネストの深さ)ごとに、aliasは合計3つまで

全体コードは以下。

github.com

実装のコアは以下。

#[async_trait]
impl Extension for RestrictQueryAliasesImpl {
    async fn parse_query(
        &self,
        ctx: &ExtensionContext<'_>,
        query: &str,
        variables: &Variables,
        next: NextParseQuery<'_>,
    ) -> ServerResult<ExecutableDocument> {
        let result = next.run(ctx, query, variables).await?;

        // after parsed
        let query_selection_set = find_query(&result);

        let aliases = alias_count_recursive(&query_selection_set);
        if aliases > self.limit {
            Err(ServerError::new(
                format!("エイリアスは全部で{}個より多くできません", self.limit),
                None,
            ))?;
        }

        let aliases_per_level = alias_count_by_nested_level_recursive(&query_selection_set, 1);
        let max_aliases_per_level = max_value_or(aliases_per_level, 0);
        if max_aliases_per_level > self.limit_per_level {
            Err(ServerError::new(
                format!(
                    "エイリアスは各階層で{}個より多くできません",
                    self.limit_per_level
                ),
                None,
            ))?;
        }

        Ok(result)
    }
}

このextensionをSchemaBuilderで s.extension(RestrictQueryAliases::default()) として作成すればOK。

同じ階層に4つ以上aliasをいれてリクエストをすると以下のようにエラーになる。

{
  alias0: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    name
  }
  alias1: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    name
  }
  alias2: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    name
  }
  alias3: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    n0: name
    n1: name
    n2: name
    ownedBoards {
      b0: id
      b1: id
      b2: id
      owner {
        id
      }
    }
  }
}
{
  "data": null,
  "errors": [
    {
      "message": "エイリアスは各階層で3個より多くできません"
    }
  ]
}

同じ階層ではaliasを3つ以内に押さえていても、全体で10個以上になるリクエストは以下のようにエラーになる。

{
  alias0: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    name
  }
  alias1: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    name
  }
  alias3: user(id: "user-01HBCCGK3MG5HA7GJG25BGV6PJ") {
    n0: name
    n1: name
    n2: name
    ownedBoards {
      b0: id
      b1: id
      b2: id
      owner {
        id
        i0: id
        i1: id
      }
    }
  }
}
{
  "data": null,
  "errors": [
    {
      "message": "エイリアスは全部で10個より多くできません"
    }
  ]
}