GraphQL - Hiểu để hack (Phần 1)
I. Giới thiệu chung về GraphQL
Ở chuỗi bài viết này, chúng ta sẽ cùng tìm hiểu một số phương pháp và cách thức khai thác lỗ hổng GraphQL. Nhưng trước khi muốn hack được GraphQL, chúng ta cần hiểu nó là gì? Thành phần của nó ra sao cũng như cách thức hoạt động của GraphQ như thế nào. Ở phần 1 của series bài viết này, tôi sẽ giúp các bạn hiểu thế nào là GraphQL và cách thức hoạt động của nó.
GraphQL là một ngôn ngữ truy vấn API được thiết kế để tạo sự giao tiếp hiệu quả giữa client và server. Nó cho phép người dùng chỉ định chính xác dữ liệu mà họ muốn nhận trong phản hồi, giúp tránh việc trả về quá nhiều dữ liệu thừa của đối tượng và nhiều truy vấn như thường thấy trong REST API.
GraphQL services xác định một contract thông qua đó client có thể giao tiếp với server. Khách hàng không cần biết dữ liệu được lưu trữ ở đâu. Thay vào đó, client gửi các truy vấn đến máy chủ GraphQL, máy chủ này sẽ lấy dữ liệu từ các nơi liên quan. Vì GraphQL không phụ thuộc vào nền tảng, nó có thể được triển khai với nhiều ngôn ngữ lập trình khác nhau và có thể được sử dụng để giao tiếp với hầu hết các cơ sở dữ liệu khác nhau.
Graphql cũng hỗ trợ cho quá trình phát triển API trở nên đơn giản và dễ dàng hơn theo thời gian thực. Graphql được phát triển dựa theo ba đặc trưng chính bao gồm:
- Graphql sẽ làm cho việc tổng hợp dữ liệu đến từ nhiều nguồn khác nhau trở nên dễ dàng hơn.
- Graphql cho phép các client có khả năng định dạng các dữ liệu mà họ cần một cách chính xác.
- Graphql thường sử dụng type system để phục vụ cho mục đích khai báo dữ liệu.
II. Cách thức hoạt động của GraphQL
Schema GraphQL xác định cấu trúc dữ liệu của dịch vụ, liệt kê các đối tượng có sẵn (được gọi là types), các trường và mối quan hệ. Dữ liệu mà schema GraphQL mô tả có thể được xử lý bằng ba loại hoạt động:
- Truy vấn (Queries): Lấy dữ liệu.
- Thay đổi (Mutations): Thêm, thay đổi hoặc xóa dữ liệu.
- Đăng ký (Subscriptions): Tạo một kết nối vĩnh viễn giữa máy chủ và khách hàng, cho phép máy chủ đẩy dữ liệu thời gian thực đến khách hàng trong định dạng đã chỉ định.
Tất cả các toán tử (operations) GraphQL sử dụng cùng một endpoint, thường được gửi dưới dạng yêu cầu POST. Điều này khác biệt đáng kể so với REST API, nơi mà các hoạt động được gửi đến các endpoint cụ thể dựa trên các phương thức HTTP khác nhau. Trong GraphQL, type và name của các toán tử (operation) xác định cách truy vấn được xử lý, thay vì điểm cuối nó được gửi đến hoặc phương thức HTTP được sử dụng. Dịch vụ GraphQL thường phản hồi các hoạt động bằng một đối tượng JSON theo cấu trúc yêu cầu.
Contract: Trong GraphQL, một contract (hợp đồng) là một tài liệu mô tả chi tiết về cách một GraphQL service hoạt động và các thao tác (operations) mà nó hỗ trợ. Contract định nghĩa các loại dữ liệu (types), trường (fields), đối số (arguments), và các hoạt động mà khách hàng có thể gửi đến GraphQL service để truy vấn dữ liệu hoặc thay đổi dữ liệu.
Contract đóng vai trò quan trọng trong việc xác định các cách tổ chức dữ liệu được và truy cập trong GraphQL service, giúp client hiểu rõ các loại dữ liệu có sẵn và cách tương tác với service.
Có hai phần quan trọng trong contract GraphQL:
Schema: Schema là phần chính của contract GraphQL. Nó định nghĩa cấu trúc dữ liệu và thể hiện các loại dữ liệu, trường, đối số và các hoạt động có sẵn trong GraphQL service. Schema sử dụng một ngôn ngữ đặc biệt gọi là Schema Definition Language (SDL) để mô tả các loại và mối quan hệ giữa chúng.
Operations: Contract GraphQL cũng định nghĩa các hoạt động mà khách hàng có thể thực hiện trên service, bao gồm các truy vấn (query), biến đổi (mutation) và các hoạt động khác như đăng ký (subscription).
III. Thành phần chính của GraphQL
1. GraphQL Schema
Trong GraphQL, schema đại diện cho contract giữa phía frontend và backend. Nó định nghĩa dữ liệu có sẵn dưới dạng một loạt các loại (types), sử dụng một ngôn ngữ định nghĩa schema. Do đó, các loại này có thể được triển khai bởi dịch vụ.
Hầu hết các loại được xác định là các loại đối tượng (object types), chúng xác định các đối tượng có sẵn và các trường và đối số chúng có. Mỗi trường có kiểu riêng, có thể là một đối tượng khác hoặc một loại scalar, enum, union, interface hoặc tùy chỉnh.
Ví dụ dưới đây cho thấy một định nghĩa schema đơn giản cho loại Product. Toán tử !
chỉ ra rằng trường ID
không thể null khi được gọi (tức là bắt buộc).
#Example schema definition
type Product {
id: ID!
name: String!
description: String!
price: Int
}
Các schema cũng phải bao gồm ít nhất một truy vấn có sẵn. Thông thường, chúng cũng chứa thông tin về các biến đổi (mutations) có sẵn.
2. GraphQL Queries
GraphQL Queries được sử dụng để truy xuất dữ liệu từ kho dữ liệu (data store). Chúng tương đương với các yêu cầu GET trong REST API. Truy vấn thường bao gồm các thành phần chính sau:
- A
query
operation type (Kiểu toán tử truy vấn) : Mặc dù không bắt buộc, nhưng nên sử dụng, để xác định rõ ràng rằng yêu cầu đang được gửi là một truy vấn. - A query name (Tên truy vấn): Tên này có thể là bất kỳ thứ gì bạn muốn. Tên truy vấn không bắt buộc, nhưng khuyến khích sử dụng để giúp gỡ lỗi dễ dàng hơn.
- A data structure (Cấu trúc dữ liệu): Đây là dữ liệu mà truy vấn sẽ trả về.
- Optionally, one or more arguments (Tùy chọn, một hoặc nhiều đối số): Được sử dụng để tạo truy vấn trả về chi tiết của một đối tượng cụ thể (ví dụ: "cung cấp tên và mô tả của sản phẩm có ID là 123").
Ví dụ dưới đây cho thấy một truy vấn được gọi là myGetProductQuery yêu cầu trả về trường name và description của một sản phẩm có id là 123.
# Example query
query myGetProductQuery {
getProduct(id: 123) {
name
description
}
}
Lưu ý rằng loại sản phẩm có thể chứa nhiều trường hơn trong schema so với những gì được yêu cầu ở đây. Khả năng yêu cầu chỉ các dữ liệu cần thiết là một phần quan trọng của tính linh hoạt của GraphQL.
3. GraphQL Mutations
GraphQL Mutations (biến đổi) nhằm mục đích thay đổi dữ liệu bằng cách thêm, sửa đổi hoặc xóa dữ liệu. Chúng tương đương với các phương thức POST, PUT và DELETE của REST API.
Giống như các truy vấn, Mutation có kiểu toán tử, tên và cấu trúc cho dữ liệu trả về. Tuy nhiên, mutation luôn có một đầu vào (input) của một loại nào đó. Điều này có thể là giá trị inline, nhưng thường thì nó được cung cấp dưới dạng biến.
Ví dụ dưới đây cho thấy một mutation để tạo một sản phẩm mới và phản hồi tương ứng. Trong trường hợp này, dịch vụ được cấu hình để tự động gán một ID cho sản phẩm mới, và ID này được trả lại như yêu cầu.
REQUEST:
# Example mutation request
mutation {
createProduct(name: "Flamin' Cocktail Glasses", listed: "yes") {
id
name
listed
}
}
RESPONSE:
# Example mutation response
{
"data": {
"createProduct": {
"id": 123,
"name": "Flamin' Cocktail Glasses",
"listed": "yes"
}
}
}
IV. Các thành phần của queries và mutations
Cú pháp GraphQL bao gồm một số thành phần chung cho cả truy vấn và biến đổi.
1. Trường (Fields)
Tất cả các loại GraphQL chứa các thành phần dữ liệu có thể truy vấn, gọi là các trường (fields). Khi bạn gửi một query (truy vấn) hoặc mutation (biến đổi), bạn chỉ định những trường nào bạn muốn API trả về. Phản hồi sẽ phản ánh nội dung được chỉ định trong yêu cầu.
Ví dụ dưới đây cho thấy một truy vấn yêu cầu trả về ID và tên của tất cả nhân viên, cùng với phản hồi tương ứng.
# Request
query myGetEmployeeQuery {
getEmployees {
id
name {
firstname
lastname
}
}
}
# Response
{
"data": {
"getEmployees": [
{
"id": 1,
"name": {
"firstname": "Carlos",
"lastname": "Montoya"
}
},
{
"id": 2,
"name": {
"firstname": "Peter",
"lastname": "Wiener"
}
}
]
}
}
2. Đối số (Arguments)
Các đối số là các giá trị được cung cấp cho các trường cụ thể. Các đối số có thể được chấp nhận cho một loại được xác định trong schema.
Khi bạn gửi một query (truy vấn) hoặc mutaton (biến đổi) chứa các đối số, máy chủ GraphQL xác định cách phản hồi dựa trên cấu hình của nó. Ví dụ, nó có thể trả về một đối tượng cụ thể thay vì chi tiết của tất cả các đối tượng.
Ví dụ dưới đây cho thấy một truy vấn getEmployee sử dụng một đối số ID nhân viên. Trong trường hợp này, máy chủ trả về chỉ chi tiết của nhân viên có ID phù hợp.
# Example query with arguments
query myGetEmployeeQuery {
getEmployees(id: 1) {
name {
firstname
lastname
}
}
}
#Response to query
{
"data": {
"getEmployees": [
{
"name": {
"firstname": "Carlos",
"lastname": "Montoya"
}
}
]
}
}
3. Biến (Variables)
Biến cho phép truyền vào các đối số động, thay vì có các đối số trực tiếp trong chuỗi truy vấn.
Các truy vấn hoặc biến đổi sử dụng biến có cấu trúc giống như truy vấn sử dụng đối số inline, nhưng một số khía cạnh của truy vấn được lấy từ một từ điển biến JSON riêng biệt. Điều này giúp bạn tái sử dụng một cấu trúc chung giữa nhiều truy vấn, chỉ thay đổi giá trị của biến.
Khi xây dựng một truy vấn hoặc biến đổi sử dụng biến, bạn cần:
- Khai báo biến và loại dữ liệu của nó.
- Thêm tên biến vào vị trí thích hợp trong truy vấn.
- Truyền cặp khóa và giá trị của biến từ từ điển biến. Ví dụ dưới đây cho thấy cùng một truy vấn như trong ví dụ trước, nhưng ID được chuyển dưới dạng biến thay vì là một phần trực tiếp của chuỗi truy vấn.
#Example query with variable
query getEmployeeWithVariable($id: ID!) {
getEmployees(id: $id) {
name {
firstname
lastname
}
}
}
Variables:
{
"id": 1
}
} Trong ví dụ này, biến được khai báo trong dòng đầu tiên với ($id: ID!). Dấu ! chỉ ra rằng đây là một trường bắt buộc cho truy vấn này. Sau đó, biến được sử dụng như một đối số trong dòng thứ hai với (id: $id). Cuối cùng, giá trị của biến được đặt trong từ điển JSON biến.
4. Aliases
Các đối tượng GraphQL không thể chứa nhiều thuộc tính có cùng tên. Ví dụ, truy vấn dưới đây là không hợp lệ vì nó cố gắng trả về hai lần loại sản phẩm.
# Invalid query
query getProductDetails {
getProduct(id: 1) {
id
name
}
getProduct(id: 2) {
id
name
}
}
Alias (Đặt tên) cho phép bạn bỏ qua ràng buộc này bằng cách đặt tên các thuộc tính bạn muốn API trả về. Bạn có thể sử dụng alias để trả về nhiều phiên bản của cùng một loại đối tượng trong một yêu cầu. Điều này giúp giảm số lượng cuộc gọi API cần thiết.
Ví dụ dưới đây, truy vấn sử dụng đặt tên để chỉ định tên duy nhất cho hai sản phẩm. Truy vấn này hiện tại đã vượt qua kiểm tra hợp lệ và chi tiết sản phẩm được trả về.
# Valid query using aliases
query getProductDetails {
product1: getProduct(id: "1") {
id
name
}
product2: getProduct(id: "2") {
id
name
}
}
# Response to query
{
"data": {
"product1": {
"id": 1,
"name": "Juice Extractor"
},
"product2": {
"id": 2,
"name": "Fruit Overlays"
}
}
}
5. Đoạn mã (Fragments)
Đoạn mã cho phép tái sử dụng các phần của các truy vấn hoặc biến đổi. Chúng chứa một tập hợp con của các trường thuộc loại liên quan.
Sau khi được xác định, chúng có thể được bao gồm trong các truy vấn hoặc biến đổi. Nếu chúng sau đó thay đổi, thay đổi này được áp dụng trong mọi truy vấn hoặc biến đổi sử dụng đoạn mã.
Ví dụ dưới đây cho thấy một truy vấn getProduct trong đó chi tiết sản phẩm được đặt trong một đoạn mã productInfo.
# Example fragment
fragment productInfo on Product {
id
name
listed
}
#Query calling the fragment
query {
getProduct(id: 1) {
...productInfo
stock
}
}
# Response including fragment fields
{
"data": {
"getProduct": {
"id": 1,
"name": "Juice Extractor",
"listed": "no",
"stock": 5
}
}
}
6. Subscriptions
Subscriptions (Đăng ký) là một loại đặc biệt của truy vấn. Chúng cho phép client thiết lập và giữ một kết nối với máy chủ để máy chủ có thể đẩy cập nhật thời gian thực đến khách hàng mà không cần phải liên tục kiểm tra dữ liệu. Điều này hữu ích chủ yếu cho các thay đổi nhỏ trên các đối tượng lớn và cho các chức năng yêu cầu cập nhật thời gian thực nhỏ (như hệ thống trò chuyện hoặc chỉnh sửa cộng tác).
Giống như các truy vấn và biến đổi thông thường, yêu cầu đăng ký xác định hình dạng của dữ liệu cần trả về.
Subscriptions thường được triển khai bằng cách sử dụng WebSockets.
7. Introspection
Introspection là một chức năng GraphQL tích hợp sẵn cho phép bạn truy vấn máy chủ để xem thông tin về schema. Nó thường được sử dụng bởi các ứng dụng như các công cụ IDE GraphQL và công cụ tạo tài liệu.
Tương tự như các truy vấn thông thường, bạn có thể chỉ định các trường và cấu trúc của phản hồi bạn muốn trả về. Ví dụ, bạn có thể muốn phản hồi chỉ chứa tên của các biến đổi có sẵn.
Introspection có thể tạo ra nguy cơ rò rỉ thông tin nghiêm trọng, vì nó có thể được sử dụng để truy cập thông tin nhạy cảm (như mô tả trường) và giúp kẻ tấn công tìm hiểu cách tương tác với API. Tốt nhất là tắt Introspection trong môi trường production.
V. Tổng kết
Nội dung bài viết tập trung vào khái niệm cơ bản của GrapQL để giúp người đọc hiểu được các thành phần của GraphQL. Ở phần tiếp theo, chúng ta sẽ đến với chủ đề GraphQL API vulnerabilities. Rất mong các bạn sẽ ủng hộ và đón đọc.
Nguồn tham khảo trong bài viết:
All Rights Reserved