Khi học về an toàn của api , chúng ta thường tiếp cận REST trước rồi mới tới GraphQL. Điều này khá tự nhiên vì REST xuất hiện sớm, dễ hình dung và có cấu trúc endpoint rõ ràng. Tuy nhiên, trong nhiều hệ thống hiện đại, GraphQL ngày càng được dùng rộng rãi vì nó cho phép phía client yêu cầu đúng dữ liệu mình cần, không thừa cũng không thiếu. Chính sự linh hoạt đó đem lại hiệu năng và trải nghiệm phát triển tốt hơn, nhưng đồng thời cũng làm cho bề mặt tấn công trở nên tinh vi hơn.
Một điểm rất quan trọng cần hiểu ngay từ đầu là GraphQL không tự sinh ra lỗ hổng theo nghĩa tuyệt đối. Phần lớn các vấn đề bảo mật trong GraphQL vẫn xoay quanh các lỗi đã quen thuộc như kiểm soát truy cập yếu, xác thực không chặt, xử lý đầu vào không an toàn, tiêu tốn tài nguyên quá mức và lộ thông tin nội bộ. Tuy nhiên, vì GraphQL gom nhiều chức năng vào một điểm truy cập duy nhất và cho phép truy vấn sâu theo cấu trúc dữ liệu, nên chỉ một sai sót nhỏ trong logic kiểm soát cũng có thể dẫn đến hậu quả lớn.
Muốn kiểm thử GraphQL tốt, trước hết phải hiểu GraphQL là gì, nó hoạt động ra sao, rồi mới đi vào từng nhóm lỗ hổng. Nếu chỉ nhìn một truy vấn GraphQL như một đoạn cú pháp lạ mắt thì rất khó nhận ra tại sao một hệ thống có vẻ hợp lệ lại vẫn bị đọc dữ liệu trái phép, sửa dữ liệu trái phép, xóa dữ liệu trái phép hay bị tấn công từ chối dịch vụ (DoS).
Một số basic về GraphQL
GraphQL là một ngôn ngữ truy vấn dữ liệu cho API và đồng thời là một cơ chế thực thi truy vấn ở phía máy chủ. Nói đơn giản, nó cho phép client mô tả rất rõ mình muốn lấy dữ liệu nào, ở cấp độ nào, và mối quan hệ giữa các đối tượng ra sao. Khác với REST, nơi mỗi nhóm chức năng thường được tách thành nhiều endpoint như /users, /orders, /products, GraphQL thường tập trung vào một endpoint chung, ví dụ /graphql. Phần khác biệt không nằm ở URL mà nằm trong nội dung truy vấn gửi lên server. Ví dụ, nếu phía client chỉ muốn lấy id và email của người dùng hiện tại, nó có thể gửi một truy vấn rất ngắn như sau:
</> GraphQL
query {
me {
id
email
}
}Ở đây, truy vấn đọc dữ liệu được gọi là truy vấn đọc (query). Query chỉ dùng để lấy dữ liệu, không nên làm thay đổi trạng thái hệ thống.
Nếu muốn thay đổi dữ liệu, ta dùng thao tác thay đổi dữ liệu (mutation). Ví dụ:
</> GraphQL
mutation {
updateEmail(id: "15", email: "[email protected]") {
id
email
}
}Mutation được dùng cho các hành động như tạo, cập nhật, xóa hoặc kích hoạt một hành vi có tác động tới dữ liệu.
Ngoài ra còn có cơ chế đăng ký nhận thay đổi thời gian thực (subscription), nhưng trong phạm vi bài này, phần trọng tâm là query và mutation vì đó là nơi đa số lỗ hổng thực chiến xuất hiện.
Schema là gì?
Muốn hiểu GraphQL, bắt buộc phải hiểu lược đồ dữ liệu (schema). Schema là bản mô tả toàn bộ cấu trúc dữ liệu mà API hỗ trợ. Nó nói rõ hệ thống có những kiểu dữ liệu nào, mỗi kiểu có những trường nào, trường nào là bắt buộc, và client được phép gọi những thao tác nào. Ví dụ schema đơn giản:
</> GraphQL
type User {
id: ID!
email: String!
role: String!
}
type Query {
me: User
user(id: ID!): User
}
type Mutation {
updateEmail(id: ID!, email: String!): User
deleteUser(id: ID!): Boolean
}Schema trên cho biết có kiểu User với ba trường là id, email và role. Nó cũng cho biết client có thể gọi hai query là me và user, cùng hai mutation là updateEmail và deleteUser.
Đây là điểm khiến GraphQL vừa mạnh vừa nhạy cảm. Chỉ cần nhìn được schema, người kiểm thử có thể hình dung khá rõ bề mặt chức năng của hệ thống. Nếu schema để lộ quá nhiều thông tin ở môi trường production, kẻ tấn công sẽ dễ dàng biết nên thử những thao tác nào, trên kiểu dữ liệu nào, với tham số gì.
Resolver là gì?
Schema chỉ mô tả hình dạng của dữ liệu, còn dữ liệu thực sự được lấy hay thay đổi bằng bộ xử lý trường (resolver). Resolver là hàm phía server nhận đầu vào từ truy vấn GraphQL rồi thực hiện logic tương ứng, ví dụ đọc cơ sở dữ liệu (database), gọi một dịch vụ khác, kiểm tra quyền, hoặc biến đổi dữ liệu trước khi trả về. Ví dụ resolver đơn giản chạy được, nhưng nó mới chỉ làm một việc là nhận id rồi trả về user tương ứng. Nếu không kiểm tra xem người gọi có được phép xem user đó hay không, resolver này sẽ trở thành nguồn gốc của lỗ hổng truy cập dữ liệu trái phép.
</> GraphQL
const resolvers = {
Query: {
user: async (_, { id }, ctx) => {
return db.users.findById(id);
}
}
};Trong GraphQL, rất nhiều vấn đề an ninh không nằm ở schema mà nằm ở resolver. Schema chỉ nói có chức năng này. Resolver mới là thằng quyết định chức năng đó an toàn hay không.
Tại sao GraphQL dễ phát sinh lỗ hổng hơn người mới tưởng?
Có ba lý do chính.
Thứ nhất, GraphQL rất linh hoạt. Client có thể xin đúng những trường mình muốn, đi sâu qua nhiều cấp quan hệ dữ liệu trong một request duy nhất. Nếu phía server không kiểm soát chặt, chỉ một truy vấn cũng có thể lấy ra lượng dữ liệu vượt quá dự kiến.
Thứ hai, GraphQL thường gom nhiều chức năng vào một endpoint chung. Điều này khiến nhiều đội phát triển lầm tưởng rằng nếu đã bảo vệ endpoint /graphql thì coi như đã an toàn. Thực ra bên trong endpoint đó có thể tồn tại hàng chục query và mutation khác nhau, mỗi cái có logic nghiệp vụ riêng và mức độ nhạy cảm khác nhau.
Thứ ba, nhiều nhà phát triển kiểm tra xác thực (authentication) nhưng quên kiểm tra phân quyền (authorization). Xác thực chỉ trả lời câu hỏi ai đang gọi. Phân quyền mới trả lời được phép làm gì, trên đối tượng nào, trong hoàn cảnh nào. Rất nhiều lỗi GraphQL xuất hiện chính ở khoảng trống này.
Nguy cơ 1: Truy cập dữ liệu trái phép (Broken Object Level Authorization, BOLA)
Đây là một trong những lỗi phổ biến nhất. Bản chất của lỗi là hệ thống cho phép người dùng đã đăng nhập đọc dữ liệu của đối tượng mà họ không có quyền xem .
Ví dụ resolver lỗi:
</> javascript
const resolvers = {
Query: {
user: async (_, { id }, ctx) => {
if (!ctx.user) {
throw new Error("Unauthenticated");
}
return db.users.findById(id);
}
}
};Đoạn code này kiểm tra rằng người gọi đã đăng nhập. Nhưng sau đó nó lấy id do client cung cấp và truy vấn thẳng cơ sở dữ liệu. Điều đó có nghĩa là người dùng A có thể đổi id thành của người dùng B để đọc email, vai trò hoặc thông tin khác của B.
Ví dụ truy vấn:
</> GraphQL
query {
user(id: "42") {
id
email
role
}
}Nếu server không kiểm tra quyền trên đối tượng User có id bằng 42, truy vấn này sẽ trả về dữ liệu của người khác.
Cách sửa đúng không phải chỉ là kiểm tra đăng nhập, mà là kiểm tra quan hệ giữa người gọi và đối tượng cần truy cập:
</> javascript
const resolvers = {
Query: {
user: async (_, { id }, ctx) => {
if (!ctx.user) {
throw new Error("Unauthenticated");
}
const target = await db.users.findById(id);
if (!target) {
return null;
}
const canRead =
ctx.user.role === "admin" || ctx.user.id === target.id;
if (!canRead) {
throw new Error("Forbidden");
}
return target;
}
}
};Ở đây, nếu người gọi là admin hoặc owner của đối tượng, truy cập được phép. Nếu không, phải chặn.
Điểm cần nhớ là trong GraphQL, mỗi resolver đọc dữ liệu đều nên được xem là một điểm cần kiểm tra phân quyền độc lập. Không được giả định rằng frontend đã ẩn nút hoặc không hiển thị chức năng đó thì backend sẽ an toàn.
Nguy cơ 2: Tạo hoặc sửa dữ liệu trái phép
Nếu lỗi đọc dữ liệu trái phép làm mất tính bí mật (confidentiality), thì lỗi sửa dữ liệu trái phép làm mất tính toàn vẹn (integrity). Đây là nhóm lỗi xảy ra ở mutation khi hệ thống cho phép người dùng không đủ quyền tạo, sửa hoặc kích hoạt các thay đổi nghiệp vụ đáng lẽ không được phép. Ví dụ lỗi điển hình:
</> javascript
const resolvers = {
Mutation: {
updateRole: async (_, { userId, role }, ctx) => {
if (!ctx.user) {
throw new Error("Unauthenticated");
}
return db.users.update(userId, { role });
}
}
};Thoạt nhìn, đoạn code có vẻ hợp lý vì nó yêu cầu người dùng đăng nhập. Nhưng thực ra nó cực kỳ nguy hiểm. Bất kỳ tài khoản hợp lệ nào cũng có thể gọi mutation này để đổi role của bất kỳ người dùng nào. Kẻ tấn công có thể thử mutation như sau:
</> GraphQL
mutation {
updateRole(userId: "42", role: "admin") {
id
role
}
}Nếu thành công, đây chính là leo thang đặc quyền (privilege escalation). Cách sửa là phải kiểm tra xem ai được phép thay đổi role, đồng thời giới hạn giá trị đầu vào:
</> javascript
const resolvers = {
Mutation: {
updateRole: async (_, { userId, role }, ctx) => {
if (!ctx.user) {
throw new Error("Unauthenticated");
}
if (ctx.user.role !== "admin") {
throw new Error("Forbidden");
}
const allowedRoles = new Set(["user", "manager", "auditor"]);
if (!allowedRoles.has(role)) {
throw new Error("Invalid role");
}
return db.users.update(userId, { role });
}
}
};Khi đọc loại code này, sinh viên nên tự tập đặt ba câu hỏi. Ai đang gọi. Họ đang thay đổi dữ liệu của ai. Và hệ thống đã kiểm tra điều kiện nghiệp vụ chưa. Nếu một trong ba câu hỏi chưa có câu trả lời rõ ràng, rất có thể mutation đó đang chứa lỗ hổng.
Nguy cơ 3: Xóa dữ liệu trái phép
Xóa dữ liệu thường là hành động nguy hiểm hơn đọc hoặc sửa vì hậu quả có thể khó đảo ngược. Trong nhiều hệ thống, giao diện người dùng ẩn nút xóa với tài khoản thường, nhưng mutation phía sau vẫn tồn tại và có thể bị gọi trực tiếp.
</>javascript
const resolvers = {
Mutation: {
deleteReport: async (_, { reportId }, ctx) => {
if (!ctx.user) {
throw new Error("Unauthenticated");
}
await db.reports.delete(reportId);
return true;
}
}
};Ở đây, ai đăng nhập cũng xóa được report nếu biết reportId. Đây là lỗi rất nghiêm trọng trong hệ thống quản trị, hệ thống cộng tác, hệ thống học tập, quản lý hồ sơ hoặc tài liệu nội bộ. Cách sửa:
</> javascript
const resolvers = {
Mutation: {
deleteReport: async (_, { reportId }, ctx) => {
if (!ctx.user) {
throw new Error("Unauthenticated");
}
const report = await db.reports.findById(reportId);
if (!report) {
return false;
}
const canDelete =
ctx.user.role === "admin" ||
report.ownerId === ctx.user.id;
if (!canDelete) {
throw new Error("Forbidden");
}
await db.reports.delete(reportId);
return true;
}
}
};Trong thực tế, không nên chỉ nghĩ tới quyền. Với thao tác xóa, còn phải cân nhắc ghi nhật ký kiểm toán (audit log), xóa mềm (soft delete), xác nhận lại hành động nhạy cảm, và ràng buộc trạng thái nghiệp vụ. Ví dụ hóa đơn đã phát hành, kết quả thi đã chốt, hoặc log an ninh đã ký số thì không thể xóa tự do như record thông thường.
Nguy cơ 4: Tấn công từ chối dịch vụ do truy vấn quá nặng (DoS)
GraphQL cho phép truy vấn dữ liệu lồng nhau. Đây là điểm rất mạnh, nhưng cũng là nguồn gốc của nhiều cuộc tấn công từ chối dịch vụ. Một request duy nhất có thể khiến server phải thực hiện số lượng công việc rất lớn nếu truy vấn đi quá sâu hoặc lấy quá nhiều phần tử ở nhiều cấp. Ví dụ schema:
</> GraphQL
type User {
id: ID!
name: String!
friends(limit: Int = 10): [User!]!
}
type Query {
me: User
}Một truy vấn nặng có thể như sau:
</> GraphQL
query {
me {
friends(limit: 50) {
friends(limit: 50) {
friends(limit: 50) {
id
name
}
}
}
}
}Query này có vẻ ngắn, nhưng nó có thể khiến hệ thống truy vấn hàng chục nghìn bản ghi hoặc gọi lặp nhiều tầng resolver. Nếu backend không có cơ chế giới hạn, chỉ cần vài request như vậy cũng đủ làm server chậm hoặc cạn tài nguyên. Đây là lý do tại sao trong GraphQL không thể chỉ nhìn số lượng request mỗi giây. Một request có thể rất nhỏ về kích thước nhưng cực lớn về chi phí thực thi.
Các biện pháp phòng thủ gồm có:
Trước hết là phân trang (pagination) cho mọi field trả về danh sách. Không nên cho phép lấy hàng nghìn bản ghi một lần nếu không thật sự cần. Ví dụ:
</> GraphQL
type User {
id: ID!
name: String!
friends(limit: Int = 10, offset: Int = 0): [User!]!
}Tiếp theo là giới hạn độ sâu (Query depth) truy vấn. Có thể viết logic ước lượng độ sâu của cây truy vấn rồi chặn nếu vượt ngưỡng.
</> javascript
function validateDepth(documentAst, maxDepth = 5) {
const depth = estimateDepth(documentAst);
if (depth > maxDepth) {
throw new Error("Query depth exceeded");
}
}Ngoài độ sâu, còn phải xét chi phí truy vấn (query complexity). Một truy vấn sâu vừa phải nhưng mỗi tầng lấy 1000 phần tử vẫn rất nguy hiểm.
</> javascript
function estimateCost({ depth, listSizes }) {
return depth * listSizes.reduce((a, b) => a * b, 1);
}Cuối cùng là giới hạn tần suất gọi (rate limiting), theo IP, theo token, theo tenant hoặc theo tài khoản. Nếu hệ thống có nhiều khách hàng dùng chung hạ tầng, cần bảo vệ để một tenant không làm cạn tài nguyên của tenant khác.
Nguy cơ 5: SQL injection trong Resolver
Nhiều người mới học nghĩ rằng dùng GraphQL thì sẽ bớt gặp SQL injection. Đây là một hiểu lầm phổ biến. GraphQL chỉ giúp chuẩn hóa cách client gửi truy vấn tới server. Còn khi resolver nhận đầu vào rồi đem nối chuỗi vào SQL, lỗ hổng injection vẫn xảy ra như thường. Ví dụ lỗi:
</> javascript
const resolvers = {
Query: {
searchUsers: async (_, { keyword }) => {
const sql =
"SELECT id, email FROM users WHERE email LIKE '%" +
keyword +
"%'";
return db.raw(sql);
}
}
};Nếu keyword chứa nội dung độc hại, truy vấn SQL có thể bị thay đổi ý nghĩa. Bản chất của lỗi không nằm ở GraphQL mà nằm ở cách lập trình viên xử lý dữ liệu đầu vào.
Nhiều sinh viên cũng nhầm giữa biến trong GraphQL và tham số trong SQL. GraphQL variable chỉ giúp truyền dữ liệu theo cách chuẩn ở tầng GraphQL. Nó không tự biến chuỗi SQL thành an toàn. Muốn an toàn ở tầng cơ sở dữ liệu, vẫn phải dùng truy vấn tham số hóa (parameterized query) hoặc ORM đúng cách.
Cách sửa:
</> javascript
const resolvers = {
Query: {
searchUsers: async (_, { keyword }) => {
const sql =
"SELECT id, email FROM users WHERE email LIKE $1";
return db.query(sql, [`%${keyword}%`]);
}
}
};Ngoài ra cần có kiểm tra đầu vào:
</> javascript
function validateKeyword(keyword) {
if (typeof keyword !== "string") {
throw new Error("Invalid input");
}
if (keyword.length > 100) {
throw new Error("Input too long");
}
}Validation không thay thế parameterized query, nhưng nó giúp giảm bề mặt lỗi và làm rõ ràng quy tắc nghiệp vụ.
Nguy cơ 6: Lộ lược đồ dữ liệu (introspection) và thông tin nội bộ
GraphQL có một tính năng rất tiện là tự mô tả introspection. Nhờ đó, công cụ phát triển có thể hỏi server xem hiện có những type nào, field nào, query nào, mutation nào. Đây là lý do các IDE GraphQL hoạt động rất thuận tiện. Ví dụ truy vấn introspection:
</> GraphQL
query {
__schema {
types {
name
}
}
}Nếu tính năng này bật ở môi trường production mà không có kiểm soát phù hợp, người kiểm thử hoặc kẻ tấn công có thể nhanh chóng vẽ lại toàn bộ bề mặt API. Họ sẽ biết chính xác có những mutation gì, tham số ra sao, kiểu dữ liệu nào nhạy cảm, và từ đó rút ngắn thời gian dò tìm. Tuy nhiên, cần hiểu đúng rằng lộ schema không phải lúc nào cũng là lỗ hổng theo nghĩa trực tiếp. Nó là rò rỉ thông tin (information disclosure). Mức độ nghiêm trọng phụ thuộc vào bối cảnh. Nếu đó là public API dành cho bên thứ ba phát triển ứng dụng, introspection có thể là tính năng hợp lý. Nhưng nếu là API nội bộ hoặc chỉ phục vụ first party client, việc để lộ schema ở production thường không cần thiết. Cách giảm rủi ro là tắt introspection ở production nếu không có nhu cầu thật sự:
</> javascript
const isProd = process.env.NODE_ENV === "production";
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: !isProd
});Ngoài ra, cần tránh trả lỗi quá chi tiết. Nhiều hệ thống dù đã tắt introspection nhưng lại để thông báo lỗi giúp attacker suy luận ra tên field, kiểu dữ liệu hoặc phần logic nội bộ.
Ví dụ che lỗi:
</> javascript
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: () => {
return { message: "Request failed" };
}
});Điểm cốt lõi là tắt introspection không làm hệ thống tự nhiên an toàn. Nó chỉ giảm khả năng thăm dò ban đầu. Phần quyết định vẫn là kiểm soát truy cập và thiết kế resolver.
Nguy cơ 7: Tấn công CSRF với GraphQL
Tấn công Cross Site Request Forgery (CSRF) xảy ra khi trình duyệt của nạn nhân tự động gửi request kèm thông tin xác thực tới một hệ thống mà nạn nhân đang đăng nhập, dù chính nạn nhân không chủ ý thực hiện hành động đó. Nhiều hệ thống GraphQL bị hiểu nhầm là an toàn trước CSRF vì chúng chỉ nghĩ theo hướng nhận JSON POST. Nhưng trên thực tế, có những trường hợp endpoint GraphQL chấp nhận cả biểu mẫu mã hóa kiểu form, hoặc hệ thống dùng cookie làm phiên đăng nhập mà không có CSRF token. Khi đó mutation vẫn có thể bị gửi từ một website khác. Ví dụ server dễ lỗi:
</> javascript
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.post("/graphql", async (req, res) => {
const query = req.body.query;
const variables = req.body.variables || {};
const sessionUser = await loadUserFromCookie(req);
const result = await executeGraphQL({
query,
variables,
user: sessionUser
});
res.json(result);
});Nếu server xác thực người dùng bằng cookie và chấp nhận nội dung dạng form, một trang độc hại có thể ép trình duyệt nạn nhân gửi mutation tới /graphql.
Để giảm rủi ro, cần ít nhất ba lớp bảo vệ.
Lớp đầu là giới hạn loại nội dung gửi lên, chỉ nhận application/json nếu đó là thiết kế mong muốn.
Lớp thứ hai là kiểm tra CSRF token.
Lớp thứ ba là không cho mutation chạy qua GET và không dựa hoàn toàn vào việc phía frontend không tạo form cho chức năng đó.
Ví dụ:
</> javascript
app.post("/graphql", async (req, res) => {
const contentType = req.headers["content-type"] || "";
if (!contentType.startsWith("application/json")) {
return res.status(415).json({ error: "Unsupported content type" });
}
const csrfToken = req.headers["x-csrf-token"];
const expected = req.cookies.csrfToken;
if (!csrfToken || csrfToken !== expected) {
return res.status(403).json({ error: "CSRF validation failed" });
}
const sessionUser = await loadUserFromCookie(req);
const result = await executeGraphQL({
query: req.body.query,
variables: req.body.variables || {},
user: sessionUser
});
res.json(result);
});Nếu hệ thống dùng token trong Authorization header thay vì cookie, rủi ro CSRF thường giảm đi đáng kể. Tuy nhiên, không nên chủ quan mà bỏ qua đánh giá toàn bộ luồng xác thực.
Một mẫu tư duy kiểm tra resolver an toàn
Khi review code GraphQL, có thể dùng một khung tư duy rất thực dụng. Với mỗi query hoặc mutation, hãy tự hỏi:
Người gọi là ai.
Đối tượng đang được truy cập là gì.
Hành động đang được thực hiện là đọc, tạo, sửa hay xóa.
Điều kiện nghiệp vụ nào phải đúng thì hành động mới hợp lệ.
Truy vấn này có thể gây tốn tài nguyên quá mức không.
Đầu vào có thể đi tiếp xuống SQL, NoSQL, command, template hay hệ thống bên ngoài không.
Thông tin trả về có làm lộ cấu trúc nội bộ hoặc dữ liệu nhạy cảm không.
Nếu trả lời được rõ ràng từng câu hỏi này, khả năng bỏ sót lỗi sẽ giảm đi rất nhiều.
Một skeleton GraphQL an toàn hơn
Dưới đây là đoạn khung minh họa gói lại các ý chính:
</> javascript
const isProd = process.env.NODE_ENV === "production";
function requireAuth(ctx) {
if (!ctx.user) {
throw new Error("Unauthenticated");
}
}
function requireOwnerOrAdmin(ctx, ownerId) {
requireAuth(ctx);
if (ctx.user.role !== "admin" && ctx.user.id !== ownerId) {
throw new Error("Forbidden");
}
}
const resolvers = {
Query: {
me: async (_, __, ctx) => {
requireAuth(ctx);
return db.users.findById(ctx.user.id);
},
user: async (_, { id }, ctx) => {
requireOwnerOrAdmin(ctx, id);
return db.users.findById(id);
}
},
Mutation: {
updateEmail: async (_, { id, email }, ctx) => {
requireOwnerOrAdmin(ctx, id);
if (typeof email !== "string" || !email.includes("@")) {
throw new Error("Invalid email");
}
return db.users.update(id, { email });
},
deleteUser: async (_, { id }, ctx) => {
requireOwnerOrAdmin(ctx, id);
await db.users.softDelete(id);
return true;
},
searchUsers: async (_, { keyword }, ctx) => {
requireAuth(ctx);
return db.query(
"SELECT id, email FROM users WHERE email LIKE $1",
[`%${keyword}%`]
);
}
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: !isProd,
formatError: () => ({ message: "Request failed" })
});Đoạn code này chưa thể xem là sản phẩm hoàn chỉnh cho production, nhưng nó thể hiện đúng tư duy nền. Có xác thực. Có phân quyền theo đối tượng. Có kiểm tra đầu vào. Có thao tác xóa mềm. Có truy vấn SQL tham số hóa. Có giảm lộ schema và lỗi chi tiết ở production.
Cuối cùng
GraphQL là một công nghệ rất mạnh cho xây dựng API hiện đại, nhưng sức mạnh đó đi kèm yêu cầu cao hơn về tư duy thiết kế an toàn. Trong GraphQL, không thể chỉ bảo vệ endpoint rồi cho rằng mọi thứ bên trong đều ổn. Mỗi query, mỗi mutation, thậm chí mỗi field resolver quan trọng đều có thể là một điểm quyết định an ninh.
Các nhóm lỗ hổng phổ biến nhất cần luôn ghi nhớ là truy cập dữ liệu trái phép, sửa dữ liệu trái phép, xóa dữ liệu trái phép, tấn công từ chối dịch vụ do truy vấn nặng, SQL injection trong resolver, lộ schema và CSRF. Nếu nhìn sâu vào bản chất, ta sẽ thấy hầu hết chúng đều quy về ba vấn đề gốc. Kiểm soát truy cập không chặt. Kiểm soát đầu vào không chặt. Kiểm soát tài nguyên thực thi không chặt.
Vì vậy, học GraphQL an toàn không nên dừng ở mức nhớ tên lỗ hổng. Điều quan trọng hơn là hình thành thói quen đọc một resolver và tự hỏi logic bảo vệ đang nằm ở đâu. Nếu câu trả lời là không rõ, rất có thể đó chính là nơi cần kiểm tra sâu hơn.



