
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명의 수의사와 연결될 수 있습니다.
* 수의사는 저장 시스템(데이터베이스)에 **Popularity**라는 속성을 가지고 있지만, 민감한 정보이므로 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` 명령을 사용하세요.
## Deploying on 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)
## Security weaknesses
### Authorization
*깨진 접근 제어*
[CWE-285](https://cwe.mitre.org/data/definitions/285.html)
#### Issue
GraphQL은 모든 요청이 전송되는 단일 엔드포인트를 기반으로 하며, 인증은 사양의 범위를 벗어나므로(내장 기능 없음), 애플리케이션이 인증 로직을 구현하는 것은 애플리케이션의 책임입니다.
제 실험실에서는 이 지점에 취약점이 있습니다. 액세스 토큰 검증이 토큰이 **veterinaryId**에 전달된 수의사에게 속하는지 확인하지 않기 때문입니다.
**예시:**
저장소에서 식별자 **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 응답이 전송된 요청의 유효성 검사가 실패할 경우 전달된 파라미터를 반영한다는 것입니다.
**예시:**
다음 GraphQL 요청을 `myInfo(accessToken: String!, veterinaryId: Int!): Veterinary` 쿼리에 전송합니다. 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 응답에서 데이터를 렌더링하는 클라이언트가 데이터를 렌더링하기 전에 이스케이프/새니타이제이션을 적용하도록 합니다.
### 리소스 고갈
[CWE-400](https://cwe.mitre.org/data/definitions/400.html)
#### 문제
클라이언트가 요청하는 데이터 양을 제어할 수 있기 때문에, GraphQL 서버가 호출하는 스토리지와 GraphQL 서버 자체(데이터를 JSON으로 직렬화하는 과정)에 리소스 고갈을 일으키는 쿼리를 요청하는 GraphQL 요청을 보낼 수 있습니다.
이 문제는 변이(mutation)를 사용하여 매개변수에 많은 양의 데이터를 보내는 방식으로도 발생할 수 있습니다(이 공격을 방지하기 위해 입력 검증을 사용할 수 있습니다).
이 문제는 구독(subscription)을 사용하여 다음과 같은 방식으로도 발생할 수 있습니다:
* 노출된 각 구독에 대해 대량의 구독자를 등록합니다.
* 구독에서 사용되는 매개변수에 많은 양의 데이터를 보냅니다.
제 실험실에서 쿼리, 특히 익명 사용자가 사용할 수 있고 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
}
}
}
}
}
}
}
}
}
}
}
}

쿼리(Query)의 경우:
사용 중인 GraphQL 서버 구현 방식에 따라, 최대 쿼리 깊이(Maximum Query Depth) 및 쿼리 복잡도(Query Complexity) 에 대해 내장된 보호 기능을 사용하세요 (사양 참조).
Java 구현의 경우, 실행 전략에 다음 2개의 계측(Instrumentation) 클래스를 추가하세요:
위 두 계측의 사용 예시는 이 클래스를 참조하세요.
뮤테이션(Mutation)/서브스크립션(Subscription)의 경우:
GraphQL에서는 클라이언트가 API 스키마에 접근하여 사용 가능한 데이터, 쿼리, 뮤테이션 및 서브스크립션을 발견할 수 있도록 인트로스펙션 기능이 제공됩니다.
참고: 인트로스펙션을 비활성화하면 서버가 GraphQL 사양 및 대부분의 클라이언트 기대에 위배되므로 주의해서 사용하세요. 비즈니스 관점에서는 접근을 필터링하는 것이 완전히 비활성화하는 것보다 좋습니다.
이는 모든 클라이언트가 스키마를 탐색하여 Type에 흥미로운 민감 정보가 노출되었는지 확인할 수 있음을 의미합니다 (이는 Mutation 또는 Subscription에 노출된 작업에 대해서도 동일한 주의사항이 적용됩니다).
GraphiQL의 문서 탐색기(Documentation Explorer) 패널 또는 이 스크립트를 사용하여 GraphQL 엔드포인트에서 노출된 스키마를 탐색할 수 있습니다.
제 실습 환경에서 실수로 Veterinary 유형에 동물병원의 민감 정보로 간주되는 인기도(popularity) 정보가 노출되었습니다.
제 실습 환경에서 이 URL을 통해 스키마 복사본을 얻을 수 있습니다.
예시:
문서 탐색기 패널을 사용하여 다음 필드를 발견했습니다:



GraphQL 엔드포인트 접근에 인증 제약 조건을 설정하여 익명 사용자에게 노출되는 것을 방지할 수 있지만, 인증된 사용자는 여전히 이 정보 스키마에 접근할 수 있습니다.
클라이언트가 민감 정보를 노출하는 유형의 구조를 볼 수 있더라도, 이 정보를 확인하려면 해당 데이터를 반환하는 쿼리/뮤테이션/서브스크립션에 대한 접근 권한이 있어야 합니다.
스키마에 정의된 유형에 민감 정보를 매핑하지 마세요.
GraphQL은 클라이언트가 데이터를 소비하는 방식을 구체화하므로, GraphQL은 연결된 저장소에 있는 모든 데이터를 노출해서는 안 되며, 노출된 GraphQL API의 비즈니스 컨텍스트에 따라 클라이언트에게 유용한 데이터만 노출해야 합니다.
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에 취약합니다. 그 이유는 Dog와 Veterinary의 고유 식별자로 순차적 정수를 사용하기 때문입니다.
예시:
GraphiQL의 문서 탐색기를 사용하면 식별자가 단순한 정수이며 순차적임을 알 수 있습니다:


IDOR 탐지를 위한 요청 쿼리:```javascript query detectIDOR { allDogs{ id,veterinary{ id } } }
응답은 Dog와 Veterinay에 대한 순차 식별자를 보여줍니다:```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를 구축할 때, 이 서버가 기본적으로 GraphQL API를 잘못된 클라이언트 범위에 노출시키는 일부 기능을 활성화할 수 있습니다.
제 실험실에서는 기본적으로 /subscriptions 경로에 WebSocket 엔드포인트가 노출되어 있고 인증이 필요하지 않기 때문에 그렇습니다 (이 문서의 Realtime Updates with Subscriptions 섹션 참조):

스키마가 Subscription 섹션에서 subscriptions을 선언하는 경우 클라이언트는 이 엔드포인트를 통해 API 데이터에 접근할 수 있습니다.
예시:
스키마를 통해 노출된 subscriptions을 확인할 수 있습니다:

만약 이 subscription 요청을 보내 newAssociation subscription으로부터 이벤트를 수신하려면:```javascript subscription subscribeToNewAssociation{ newAssociation }
이제부터 이 구독으로부터 정보를 받게 될 것임을 알리는 다음 메시지를 받습니다:```text
Your subscription data will appear here after server publication!
그리고 예를 들어 다른 브라우저에서 이 mutation request를 통해 association을 생성하면:```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`에서 호출할 수 있습니다.
**예시:**
다음 요청을 보낼 때, *domain1.local*에서 *domain2.local*로 다른 `origin`을 지정하면:```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"},...
Call from a browser:


#### 권장 사항
기본적으로 활성화된 기능을 확인하고, API의 노출에 영향을 미칠 경우 비활성화하십시오.
Subscriptions 엔드포인트에 대한:
* 구독을 노출하는 경우 스키마에 노출된 모든 구독에 인증 및 접근 제어가 적용되었는지 확인하십시오.
* 구독을 노출하지 않는 경우 WebSocket 엔드포인트를 비활성화하거나 WAF/애플리케이션 서버 수준에서 이 엔드포인트를 차단하십시오.
제 랩에서는 이 [설정 파일](https://github.com/righettod/poc-graphql/blob/HEAD/src/main/resources/application.properties)에서 다음 옵션을 설정했습니다:
* For CORS: `graphql.servlet.corsEnabled=false`
* For 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/)
* [DOYENSEC 블로그 - GraphQL 문제점](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)