ApolloServerに脆弱性対策を実装する


#はじめに

本エントリでは、`GraphQL`APIにおけるサーバサイド実装において、最も利用されている`Apollo Server`への脆弱性対策を2ポイントから実装していきます。

https://www.apollographql.com/docs/apollo-server/


# クエリの複雑さと深さの設定（GraphQL由来の脆弱性対策）

初期状態の `Apollo Server` では、クエリの複雑さと深さに制限に関する設定がされておらず、

* 攻撃者から複雑なクエリを実行される
* 攻撃者から深い階層のクエリを実行される

リスクが残っています。

この状態では、悪意のある攻撃者からサーバのリソースを無駄に消費し、サーバに負荷をかけるクエリを発行されてしまう可能性があります。
この2つの攻撃と対策について、説明していきます。

## 複雑なクエリを実行されるリスクと対策

### 攻撃イメージ
`GraphQL` では、以下のようなクエリで簡単に大量のデータを取得可能です。
`first` は SQL でいう `limit` で、取得する件数を指定する引数です。
もしデータが存在していたら、このクエリで 100 × 1000 x 1000 = 100000000 の計算量となってしまいます。


```
{
  companies(first: 100) {
    name
    groups(first: 1000) {
      name
      users(first: 1000) {
        name
      }
    }
  }
}
```

### complexity設定による対策

上記のような悪意のあるクエリ実行を防ぐために、`graphql-validation-complexity`というライブラリを利用して、クエリの複雑さに比例して大きくなる数値（=complexity）を計算、その数値が閾値に収まっている場合のみ実行されるよう制限を行います。
構築している`GraphQL`のユースケースに合わせて、複雑さの上限を設定しておけば良いでしょう。

GitHub GraphQL API のリファレンスも参考になります。
https://docs.github.com/en/graphql/overview/resource-limitations

```typescript
validationRules: [
    createComplexityLimitRule(1000000)
  ],
```

## 深い階層のクエリを実行されるリスクと対策

### 攻撃イメージ
`GraphQL`のアンチパターンとして循環参照があります。
`GraphQL`の方がお互いに結合している場合に発生し、多段にデータが紐づいてしまい、サーバーの負荷が高くなります。

例えば下記のようなクエリだと、RecipeとIngredientの間に循環関係があり、非常に高い実行コストがかかります。
悪意のあるユーザーがこのようなネストしたクエリをAPIに送信し、サーバーをクラッシュさせる可能性があります。

```
query {
  getRecipes {
    recipes {
      ingredients {
        recipes {
          ingredients {
            recipes {
              ingredients {
                # ... and so on
              }
            }
          }
        }
      }
    }
  }
}
```

### Depth Limit設定による対策

上記の対策としてクエリの深さを制限する方法があります。
`graphql-depth-limit`というライブラリを導入し、構築している`GraphQL`のユースケースに合わせて複雑さの上限を設定しておけば良いでしょう。

```typescript
  validationRules: [
    depthLimit(10)
  ],
```


# レスポンスヘッダの設定（Webサーバー由来の脆弱性対策）

初期状態の `Apollo Server` では、Webサーバー由来の脆弱性として、レスポンスヘッダにて

* 情報がキャッシュサーバやプロキシサーバのキャッシュに保存される
* コンテンツの内容からファイルの種類を推測できる
* iframeなどを呼び出せる
* httpでアクセスできてしまう

などの状態になっており、単体では脆弱性とはなりませんが、攻撃者に悪用される可能性が残っています。
このWebサーバー由来の脆弱性の内容と対策について、下記に簡潔にまとめます。

## キャッシュ設定の不備
ユーザの重要な情報がキャッシュサーバやプロキシサーバのキャッシュに保存され、他のユ
ーザが呼び出すことができる可能性があります。
それにより、ユーザの個人情報などが他のユーザに閲覧される可能性があります。

下記を`GraphQLResponse`レスポンスヘッダに追加しておくだけで対策可能です。
`Cache-Control: private, no-store, no-cache, must-revalidate Pragma: no-cache Expires:-1`

## X-Content-Type-Optionsの不備
特定のブラウザにおいてコンテンツの内容からファイルの種類を推測して実行する機能があります。
例えば、拡張子は jpg で中身が JavaScript の場合、JavaScript が実行されるなどで
す。
それにより、画像のアップローダなどでアップロードしたファイルを誤認識し、JavaScriptを実行させるような脆弱性が存在します。

下記を`GraphQLResponse`レスポンスヘッダに追加しておくだけで対策可能です。
`X-Content-Type-Options: nosniff`

## クリックジャッキング対策の不備
クリックジャッキング（iframeを使って別サイトを読み込ませ、悪意のあるサイト上の特定箇所をクリックさせることにより、別サイトの設定情報の変更などを行う攻撃手法）への対策が行われていません。 

下記を`GraphQLResponse`レスポンスヘッダに追加しておくだけで対策可能です。
`X-Frame-Options DENY もしくは X-Frame-Options SAMEORIGIN`

## Strict-Transport-Securityの不備
Strict-Transport-Securityヘッダは、Webブラウザに対してhttpの代わりにhttpsを使うように強制する機能です。
このヘッダを設定することにより、httpでアクセスされた際にhttpsを強制することができます。 

下記を`GraphQLResponse`レスポンスヘッダに追加しておくだけで対策可能です。
`Strict-Transport-Security: max-age=15724800; includeSubdomains`


#脆弱性対策の実装

## 実装サンプル

2件のパッケージを追加します。

```
npm install graphql-depth-limit // 深い階層のクエリ対策
npm install graphql-validation-complexity // 複雑なクエリ対策
```

下記が実装サンプルです。
それぞれ、`validationRules`と`formatResponse`を利用して、

* `validationRules`にてクエリの複雑さと深さの設定（GraphQL由来の脆弱性対策）
* `formatResponse`にて`GraphQLResponse`レスポンスヘッダの設定（Webサーバー由来の脆弱性対策）

を行っています。

```typescript
const server = new ApolloServer({
  validationRules: [
    depthLimit(10),
    createComplexityLimitRule(10000)
  ],
  formatResponse: (response: GraphQLResponse | null, requestContext: GraphQLRequestContext<any>) => {
    if (requestContext.response && requestContext.response.http) {
      requestContext.response.http.headers.set('Cache-Control', "private, no-store, no-cache, must-revalidate");
      requestContext.response.http.headers.set('Pragma', "no-cache");
      requestContext.response.http.headers.set('Expires', "-1");
      requestContext.response.http.headers.set('X-Content-Type-Options', "nosniff");
      requestContext.response.http.headers.set('X-Frame-Options', "DENY");
      requestContext.response.http.headers.set('Strict-Transport-Security', "max-age=15724800; includeSubdomains");
    }
    return response as GraphQLResponse;
  },
});

server.listen({ port: process.env.PORT || 4000 }).then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});
```

## レスポンスヘッダの対策結果

```
Response Headers
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8

// ここから
cache-control: private, no-store, no-cache, must-revalidate
expires: -1
pragma: no-cache
strict-transport-security: max-age=15724800; includeSubdomains
x-content-type-options: nosniff
x-frame-options: DENY
// ここまで

Content-Length: 217
ETag: W/"d9-CbCcSw/ttAs+CI2lNN5md0H9inw"
Date: Thu, 02 Sep 2021 08:03:40 GMT
Connection: keep-alive
Keep-Alive: timeout=5
```

## クエリの複雑さと深さの設定の確認

`Apollo Studio`を利用した確認が簡単です。
https://www.apollographql.com/docs/studio/

Explorerからクエリを実行して確認できます。
![ApolloStudio.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27240/9b0fa492-8607-1ba0-ae92-cb9c489609bb.jpeg)


また、`Apollo Server`と連携しておくことで、Errorログを取得することもできます。
![errorログ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27240/55a75ad3-65d1-76e7-1227-1135393ac8aa.jpeg)


#最後に

以上、`Apollo Server`へ2ポイントで脆弱性対策するための観点と実装でした。


私が所属している株式会社クアンドではエンジニア募集中です。
地域産業のアップデートをミッションに、製造業や建設業など「現場」の課題を解決するためのアプリケーションを開発しています！
私もフルフレックス＆フルリモートで働いています。
興味のある方はエントランスブックをご覧ください！

https://quando-entrance-book.super.site/