GraphQL API 취약점 (PortSwigger Academy) - 취약점 분석과 Exploit (Part 2)
GraphQL Introspection의 개념과 위험성, 그리고 공격자 입장에서 어떻게 활용하는지 정리해보자.
GraphQL 취약점
여느 기술들과 마찬가지로, GraphQL자체가 본질적으로 위험한 기술은 아니다.
구현 방식과 설계에 따라 취약점이 발생할 수 있다는 뜻이다.
예를 들어 이전 글에서 봤듯이 Introspection 기능을 운영 환경에서 그대로 켜두면
공격자가 API 구조를 쉽게 파악할 수 있고, 여기서 권한 검사를 제대로 하지 않는다면
보면 안되는 데이터도 query조작으로 탈취가 가능하다.
이런 식의 문제가 GraphQL 취약점의 대표적인 원인으로 알려져있다.
GraphQL Endpoint 찾기 (Recon 시작)
GraphQL은 REST와 달리 여러 endpoint를 사용하는 것이 아니라, 하나의 endpoint로 다양한 query를 처리하는 구조이기 때문에 모든 요청이 하나의 Endpoint로 이어진다.
따라서, 이 Endpoint를 찾는게 1순위라고 할 수 있다.
1단계: GraphQL endpoint 경로 (URL) 찾기
방법 1. 흔한 경로 찍어보기
가장 먼저 흔한 경로를 찍는것부터 시작해볼수 있겠다.
/graphql/api/api/graphql/graphql/api/graphql/graphql등 시도해볼 수 있다. 만약에 위 경로에서 응답이 없으면/v1처럼 버전등을 뒤나 앞에 붙여보는것도 방법이다.
이는, 개발자들이 GraphQL endpoint이름을 완전히 랜덤하게 짓기보다 비슷한 패턴을 많이 쓰기 때문에 사용될 수 있다.
필요하다면 Brute-Force공격을 이용해서 경로를 찾는 방법도 있다.
방법 2. 브라우저/Burp 트래픽에서 찾기
사이트를 직접 사용하면서 Burp HTTP History를 보면 프론트엔드 API를 호출하는 흔적이 나온다.
1
2
3
POST /graphql
POST /api/graphql
GET /graphql?query=...
이런 요청이 보인다면 GraphQL endpoint 후보가 될 수 있다.
방법 3. 에러메시지 확인
우리가 찾은, 혹은 접근하고있는 URL에 아무런 요청을 보냈을 때 이런 메시지가 뜰 수 있다.
1
query not present
따라서 우리는 해당 URL이 GraphQL query 형태의 요청을 기대하는 endpoint일 가능성이 높다고 추정할 수 있다.
위 방법들 처럼 endpoint 이름을 직접적으로 리턴하거나 알 수 있는건 아니지만,
해당 URL이 GraphQL일 가능성이 높은지 판단하는 힌트가 될 수 있다.
2단계: 찾은 후보 URL이 GraphQL인지 확인 해보기
__typename은 후보 endpoint가 GraphQL인지 검증할수 있는 방법이다.
__typename은 GraphQL에 기본적으로 존재하는 예약 필드(reserved field)로, 어떤 타입의 객체인지 반환한다.
예를 들어 /graphql이 의심스럽다 가정하면, 아래와 같은 요청을 보내본다.
query { __typename }
그리고 응답이 다음과 같이 온다면, 이 URL은 GraphQL endpoint라고 확인할 수 있다.
{"data":{"__typename":"query"}}
기타 GraphQL 기반 취약점 및 설정 테스트
1. Request Method 테스트
정상적인 설정이라면 POST + application/json으로 요청을 처리한다.
이 경우 브라우저에서 임의로 요청을 생성하기 어렵기 때문에 CSRF 공격에 비교적 안전하다.
하지만 취약한 경우, GET 메소드를 허용하거나, x-www-form-urlencoded Content-type을 허용한다.
이러한 요청 방식은 브라우저에서 <form> 이나 <img>, <a> 태그 등을 통해
자동으로 전송될 수 있기 때문에 CSRF 공격에 악용될 수 있다.
특히 GraphQL endpoint가 이러한 요청을 그대로 받아들이는 경우,
공격자는 피해자의 세션을 이용해 임의의 query나 mutation을 실행시킬 수 있다.
따라서 GraphQL 테스트 시에는 단순히 요청이 정상 동작하는지만 확인하는 것이 아니라,
브라우저에서 자동으로 전송 가능한 형태인지를 함께 확인해야 한다.
2. Arguent 기반 취약점 (IDOR)
예시 요청에
query {
products {
id
name
listed
}
}
id: 1,2,4가 응답됐다고 한다면, 누락된 ID를 통해 숨겨진 데이터 (id: 3)가 존재할 가능성을 추측할 수 있다. 이후에 아래와 같이 id: 3을 요청보내게 되면.
query {
product(id: 3) {
id
name
listed
}
}
UI에서는 숨겨놨지만, API는 그대로 노출되는걸 확인할 수 있다. GraphQL에서 자주 발견되는 대표적인 IDOR 취약점 패턴이다.
GragpQL 정보 수집
GraphQL의 Endpoint를 찾았다면, 내부 구조와 필드 정보를 파악하는 단계로 넘어간다. 이러한 구조 정의를 Schema라고 한다.
Schema는 introspection, suggestions, 그리고 실제 애플리케이션 트래픽 분석을 통해 파악할 수 있다.
introspection은 GraphQL 서버의 구조(Schema)를 질의할 수 있는 기능이다.
기본적으로 아래와 같은 요청을 GraphQL endpoint (예: /graphql)에 POST로 보내면 된다.
{
"query": "{__schema{queryType{name}}}"
}
BurpSuite를 사용한 예시요청과 응답:
1
2
3
4
5
6
7
POST /graphql HTTP/1.1
Host: target.com
Content-Type: application/json
{
"query": "{__schema{queryType{name}}}"
}
요청
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
}
}
}
}
응답
만약 Introspection이 막혀있다면
1
Cannot query field "__schema"
같은 에러가 발생한다. 하지만 이 부분도, 개발자가 regex로 막게되면
newline을 추가하거나, Content-Type을 변경하거나, 또는 GET 요청으로 전송하는 방식으로 우회가 가능할 수 있다. Introspection기능이 활성화가 되어있다면 공격자는 API 구조를 파악할 수 있기 때문에 위험하다.
BurpSuite 예제
Lab1 : Accessing private GraphQL posts
Lab2 : Accidental exposure of private GraphQL fields Lab3 : Finding a hidden GraphQL endpoint
alias를 이용한 rate limiting 우회
alias란, 같은 query를 여러 번 호출하기 위해 별도의 이름을 붙이는 기능이다.
GraphQL은 한 요청에서 여러 데이터를 가져올 수 있는데,
같은 필드를 여러 번 호출하면 이름이 충돌하여 에러가 발생한다.
이때 alias를 사용하면 같은 필드를 여러 번 호출할 수 있다.
예를 들어:
Case 1. alias 없는 경우
query {
user(id:1) {
username
}
user(id:2) {
username
}
}
같은 user 필드를 두 번 호출했기 때문에 에러가 발생한다.
Case 2. alias 사용하는 경우
query {
user1: user(id:1) {
username
}
user2: user(id:2) {
username
}
}
user 필드 앞에 alias를 붙여주는 것만으로도 동일한 필드를 여러 번 호출할 수 있다.
alias를 사용하면 대표적으로 rate limit 우회 공격이 가능하다.
예를 들어, 일정 횟수 이상 로그인 시도가 발생하면 서버에서 일정 시간 동안 로그인을 제한하는 경우에도 이를 우회할 수 있다.
예시:
mutation {
a: login(password:"123")
b: login(password:"456")
c: login(password:"789")
}
서버는 위 요청을 1번으로 인식하지만 실제로는 3번 시도된 것과 동일한 효과를 가진다. 위 예시에서는 3개만 사용했지만, 공격자는 이 query 안에 100개의 비밀번호를 포함시켜 전송할 수도 있다.
즉, 서버는 요청의 개수만을 기준으로 제한을 적용하고 있으며,
하나의 요청 내부에서 수행되는 operation의 개수는 고려하지 않고 있다.
따라서 GraphQL의 alias 기능은 하나의 요청 안에 여러 operation을 포함할 수 있게 해주며,
이를 악용하면 요청 수 기반의 rate limit을 쉽게 우회할 수 있다.
BurpSuite 예제
Lab4 : Bypassing GraphQL brute force protections
Suggestions 기반 정보 수집
Introspection이 비활성화된 환경에서도, GraphQL API의 구조를 유추할 수 있는 방법이 존재하는데,
그 중 하나가 Suggestions 기능이다.
GraphQL (특히 Apollo 기반 서버)은 잘못된 query를 보냈을 때, 단순히 에러를 반환하는 것이 아니라 유사한 필드를 제안해주는 경우가 있다. (Note: Apollo는 Node.js 기반 GraphQL 서버 프레임워크)
예를들어, 존재하지 않는 필드를 요청하면 다음과 같은 응답을 받을 수 있다:
1
2
There is no field "productInfo"
Did you mean "productInformation"?
단순한 에러메시지를 넘어, 실제 존재하는 *Schema**정보를 유출하는 역할을 한다.
공격자는 이러한 힌트를 기반으로 필드 이름과 구조를 추측하여, 점진적으로 API 구조를 재구성할 수 있다.
이 과정은 자동화 도구인 Clairvoyance를 통해 더욱 효율적으로 수행 가능하다고 PortSwigger에서는 설명한다.
해당 도구는 Suggestion 응답을 분석하여, introspection이 비활성화된 환경에서도 Schema를 복원하는 기능을 제공한다고 한다.
방어 전략 정리
1. Introspection 관리
실습을 통해서도 확인을 해봤지만,
Introspection은 Query/Mutation 구조, 사용 가능한 필드, 타입 정보 등을 포함하고 있기 때문에,
공격자에게 노출될 경우 API 구조를 거의 그대로 파악할 수 있어 공격에 매우 유리하게 작용한다.
내부 API 라면 Introspection을 비활성화 해야 하며,
공개 API의 경우 Introspection을 완전히 비활성화하기 어려울 수 있기 때문에 배포 전에 schema를 충분히 검토하고 민감한 필드는 반드시 제거해야 한다.
2. Suggestions (자동 추천 기능) 비활성화
Suggetions 기능은 개발 편의성을 위해 제공되지만,
공격자 입장에서는 숨겨진 schema를 유추하는 데 활용될 수 있다.
문제의 GraphQL 에러 메시지:
1
Did you mean "getUser"?
이런 에러메시지 하나로 숨겨진 필드 추측이나 schema 유추가 가능하다. 따라서 Suggestion도 비활성화 하는게 좋다.
3. Schema 설계 보안
잘못된 설계 예시:
type User {
id
username
email
password
}
이 경우 권한 검증이 미흡하다면 IDOR, 정보 노출, 계정 탈취 등의 취약점으로 이어질 수 있다.
민감한 필드는 가능한 한 schema에 포함하지 않도록 설계하고, 최소 권한 (Least Privilege) 원칙을 적용 해주자.
4. Brute-force 공격 방어
Brute-force 실습을 통해 알아봤듯, GraphQL은 구조적으로 rate limit우회가 쉬운 구조다.
mutation {
login1: login(...)
login2: login(...)
login3: login(...)
}
GraphQL에서는 하나의 요청 안에 여러 operation을 포함할 수 있기 때문에,
기존의 “요청 수 기반 rate limit”만으로는 brute force 공격을 효과적으로 방어하기 어렵다.
Brute-force 공격 방어에는 여러가지 방법이 존재한다.
방법 1. Operation 수 제한
요청 내에서 사용 가능한 alias, field, root query의 개수를 제한하면
공격자가 하나의 요청으로 수행할 수 있는 시도 횟수를 줄일 수 있다.
방법 2. Query 크기 제한
비정상적으로 긴 query는 공격 시도일 가능성이 높기 때문에 요청 바이트 수 제한을 걸어둔다.
방법 3. Query Depth 제한
user {
posts {
comments {
author {
...
}
}
}
}
depth가 깊어질수록 서버 부담이 증가하게 된다. Brute-Force에 대한 직접적인 대응방안은 아니지만 제대로 구현이 안 된 경우, DoS 공격을 통해서 서버 리소스 고갈문제가 충분히 발생할 수 있다.
이는 brute force 공격과 직접적인 관련은 없지만,
GraphQL의 nested 구조를 악용한 DoS 공격을 방지하는 데 중요한 역할을 한다.
5. CSRF 방어
다음과 같은 조건이 동시에 만족될 경우 CSRF 공격에 취약해질 수 있다.
- mutation이
GET요청으로 실행 가능 x-www-form-urlencoded허용- CSRF 토큰 없음
방법 1. JSON POST만 허용
1
Content-Type: application/json
<form>으로 전송할 수 없기 때문에 브라우저에서 자동으로 요청을 생성하기 어려워 CSRF 공격을 효과적으로 차단할 수 있다.
방법 2. Content-Type 검증
헤더 값만 확인하는 것이 아니라, 실제 body의 형식이 Content-Type과 일치하는지도 검증해야 한다.
방법 3. CSRF 토큰 적용
CSRF 토큰은 요청이 실제 애플리케이션에서 생성된 것인지 검증하는 역할을 한다.
공격자는 이 값을 알 수 없기 때문에, 토큰 검증이 제대로 구현되어 있다면 CSRF 공격을 방어할 수 있다.
방법 4. Mutation GET 요청은 금지
1
2
3
GET /graphql?query=mutation{deleteUser}
//혹은
GET /graphql?query=mutation{changePassword}
등 발생할 수 있다.
mutation이 GET 요청으로 실행 가능할 경우, <img>나 <a> 태그만으로도 공격이 가능해지기 때문에 매우 위험하다.