IDOR Sơ cấp: Kiểm soát quyền truy cập mức đối tượng
Phân tích lỗ hổng Insecure Direct Object References: khi ID trong tham số không được đối chiếu quyền sở hữu, và cách xây dựng kiểm soát truy cập mức đối tượng.
TL;DR: Insecure Direct Object References (IDOR), hay theo danh mục OWASP API Security Top 10 là Broken Object-Level Authorization (BOLA), là một trong những lỗ hổng phổ biến và gây rò rỉ dữ liệu nghiêm trọng nhất trong các hệ thống hiện đại. Lỗi này xuất hiện khi ứng dụng sử dụng trực tiếp định danh (ID) do client gửi lên trong URL, tham số query hoặc JSON body để truy vấn bản ghi từ cơ sở dữ liệu mà không hề đối chiếu xem người dùng hiện tại có quyền sở hữu đối tượng đó hay không. Chỉ bằng cách thay đổi giá trị ID từ 1001 sang 1002, một tài khoản bất kỳ có thể đọc trộm hoặc sửa đổi hóa đơn, sao kê ngân hàng của người khác. Giải pháp kỹ thuật bắt buộc là tích hợp việc kiểm tra quyền sở hữu đối tượng ngay tại tầng truy vấn dữ liệu.
1. Lỗ hổng IDOR phát sinh từ giả định sai lầm nào?
Trong mô hình ranh giới tin cậy 01-threat-modeling-va-mo-hinh-stride, chúng ta đã xác định rằng mọi dữ liệu do máy khách gửi lên đều không đáng tin cậy. Tuy nhiên, rất nhiều lập trình viên khi thiết kế API thường mắc phải một giả định ngây thơ:
"Người dùng chỉ nhấp vào các nút bấm trên giao diện web, mà giao diện chỉ hiển thị đường link dẫn đến các tài liệu của chính họ. Do đó, tham số
idgửi lên chắc chắn là hợp lệ."
Hãy tưởng tượng một ứng dụng khám bệnh trực tuyến. Khi bệnh nhân Alice xem kết quả xét nghiệm, trình duyệt gửi request:
GET /api/medical-records/1001 HTTP/1.1
Host: healthcorp.com
Cookie: session_token=ALICE_SESSION_TOKEN
Nếu một kẻ tấn công (hoặc chính Alice tò mò) mở Burp Suite hoặc đơn giản là sửa thanh địa chỉ trình duyệt thành:
GET /api/medical-records/1002 HTTP/1.1
Nếu máy chủ trả về kết quả bệnh án của bệnh nhân Bob, hệ thống đã mắc lỗ hổng Insecure Direct Object References (IDOR)!

Hình 3.4: Lỗ hổng IDOR xảy ra khi hệ thống xác thực người dùng thành công nhưng bỏ quên khâu kiểm tra phân quyền sở hữu đối tượng.
2. Phân biệt Authentication (AuthN) và Object-Level Authorization (AuthZ)
Nguyên nhân gốc rễ của IDOR là sự nhầm lẫn giữa hai khái niệm bảo mật căn bản:
Authentication (AuthN): "Bạn là ai?"
vs
Authorization (AuthZ): "Bạn có quyền làm gì trên tài nguyên cụ thể này?"
Rất nhiều hệ thống xây dựng middleware kiểm tra đăng nhập rất chặt chẽ:
- Kiểm tra cookie phiên hợp lệ.
- Kiểm tra xem người dùng có vai trò (Role) là
Patienthay không.
Nhưng sau khi middleware xác nhận "Đây là một Patient đã đăng nhập hợp lệ", controller lại truy vấn trực tiếp cơ sở dữ liệu dựa trên ID của client mà không kiểm tra xem: Bản ghi số 1002 này có thuộc về Patient đang đăng nhập hay không?
// Express controller chua lo hong IDOR nghiem trong
app.get('/api/medical-records/:recordId', authenticateUser, async (req, res) => {
const recordId = req.params.recordId;
// Chi tim kiem theo recordId ma khong kiem tra quyen so huu!
const record = await db.medicalRecord.findUnique({
where: { id: parseInt(recordId) }
});
if (!record) {
return res.status(404).json({ error: 'Record not found' });
}
return res.json(record);
});
Ở bài học 03-classic-sqli-va-co-che-lexer, bạn đã thấy Prepared Statement giúp bảo vệ câu lệnh SQL khỏi bị biến dạng cú pháp. Nhưng trong trường hợp trên, câu lệnh SQL hoàn toàn hợp lệ về mặt cú pháp và kiểu dữ liệu! Lỗ hổng ở đây nằm hoàn toàn ở Tầng Logic Nghiệp Vụ (Business Logic Flaw).
Hãy thử kiểm tra phán đoán của bạn về một biện pháp phòng vệ phổ biến:
Một kỹ sư đề xuất thay thế toàn bộ ID tự tăng (1, 2, 3...) bằng chuỗi UUID v4 ngẫu nhiên (ví dụ /api/documents/a3f1c8e0-4b21-4f9a-9e12-882190abc123) để ngăn chặn IDOR. Nhận định nào sau đây là chính xác?
3. Vì sao UUID ngẫu nhiên không thể thay thế cho phân quyền đối tượng?
Mặc dù việc sử dụng số nguyên tuần tự (1, 2, 3...) giúp kẻ tấn công dễ dàng tự động hóa quét toàn bộ cơ sở dữ liệu qua một vòng lặp curl/Python đơn giản, việc chuyển sang UUID chỉ là lớp phòng thủ phụ trợ (Defense-in-depth).
Trong thực tế, UUID của một đối tượng có thể bị rò rỉ qua rất nhiều kênh:
- Lịch sử duyệt web hoặc Header
Referer: Khi người dùng gửi link cho người khác. - Các API công khai khác: Ví dụ endpoint
/api/users/1/poststrả về danh sách bài viết có chứa kèmdocumentUuid. - Log hệ thống của bên thứ ba: Dịch vụ phân tích truy cập mạng (Analytics) hoặc CDN ghi log URL.
Một khi kẻ tấn công có trong tay UUID, nếu API không kiểm tra xem ai đang yêu cầu, dữ liệu vẫn sẽ bị đánh cắp hoàn toàn.
4. Xây dựng cơ chế kiểm soát truy cập đối tượng an toàn thế nào?
Giải pháp kỹ thuật chuẩn mực để triệt tiêu IDOR bao gồm hai tầng phòng vệ:
Tầng 1: Ràng buộc quyền sở hữu ngay tại câu truy vấn (Scoping Queries)
Thay vì chỉ tìm kiếm theo recordId, hãy luôn kết hợp điều kiện userId lấy từ phiên đăng nhập tin cậy của máy chủ (req.session.userId hoặc JWT Payload đã verify):
// Giai phap an toan: Rang buoc truc tiep userId vao truy van
app.get('/api/medical-records/:recordId', authenticateUser, async (req, res) => {
const currentUserId = req.user.id; // Lay tu phien dang nhap da xac thuc
const recordId = parseInt(req.params.recordId);
// Bat buoc ban ghi phai thuoc ve chinh nguoi dung nay
const record = await db.medicalRecord.findFirst({
where: {
id: recordId,
patientId: currentUserId // Object-Level Authorization
}
});
if (!record) {
// Tra ve 404 thay vi 403 de khong lam lo thong tin su ton tai cua ban ghi
return res.status(404).json({ error: 'Record not found' });
}
return res.json(record);
});
Tầng 2: Áp dụng Policy-Based Authorization (RBAC / ABAC)
Với các hệ thống lớn có quan hệ phân quyền phức tạp:
- RBAC (Role-Based Access Control): Kiểm soát truy cập dựa trên vai trò (Admin, Doctor, Member). RBAC trả lời câu hỏi: "Người dùng này có vai trò gì trong hệ thống?". Tuy nhiên, RBAC thuần túy không ngăn được IDOR vì hai người dùng cùng có role
DOCTORvẫn có thể đọc chéo hồ sơ bệnh nhân của nhau nếu thiếu kiểm tra quyền sở hữu đối tượng cụ thể. - ABAC (Attribute-Based Access Control): Kiểm soát truy cập dựa trên thuộc tính và ngữ cảnh đối tượng (thuộc tính người dùng, thuộc tính tài nguyên, thời gian, mối quan hệ phân công). ABAC trả lời câu hỏi: "Người dùng này có mối liên hệ hợp lệ nào với bản ghi cụ thể này trong hoàn cảnh hiện tại không?".
Hãy tách logic phân quyền thành các lớp Policy độc lập:
// Kiem tra phan quyen bang Policy ham doc lap
async function canAccessRecord(user, record) {
if (user.role === 'ADMIN') return true;
if (record.patientId === user.id) return true;
if (user.role === 'DOCTOR' && await isAssignedDoctor(user.id, record.patientId)) {
return true;
}
return false;
}
5. Tổng kết: Nguyên tắc sở hữu dữ liệu trong API hiện đại
- Không tin tưởng ID từ Client: Mọi định danh do máy khách truyền lên đều phải chịu sự kiểm định quyền sở hữu.
- Scope dữ liệu từ Phiên làm việc: Luôn ưu tiên lấy định danh người dùng từ Token/Session phía server thay vì nhận qua URL query hay Request Body.
- Phản hồi 404 thay vì 403: Khi người dùng cố tình truy cập bản ghi của người khác, trả về
404 Not Foundđể tránh rò rỉ sự tồn tại của dữ liệu (Information Disclosure).
[!TIP] Nếu bạn phát triển backend trên hệ sinh thái Java / Spring Boot và muốn tìm hiểu cách phân quyền mức đối tượng (Object-Level Authorization) bằng
@PreAuthorizevàPermissionEvaluator, hãy tham khảo bài học Method Security & Phân quyền mức đối tượng trong Spring Security.
Bài tiếp theo: 05-mini-challenge-audit-lo-hong-so-cap
- Q1Nguyên nhân cốt lõi dẫn đến lỗ hổng IDOR (Broken Object-Level Authorization - BOLA) trong ứng dụng web là gì?
- Q2Sự khác biệt căn bản giữa Xác thực (Authentication - AuthN) và Phân quyền (Authorization - AuthZ) là gì?
- Q3Tại sao việc thay thế số tự tăng (Auto-increment ID) bằng UUID v4 KHÔNG được coi là giải pháp triệt để khắc phục IDOR?
- Q4Cách thiết kế câu truy vấn cơ sở dữ liệu an toàn nhất để phòng ngừa IDOR ở tầng data access là gì?
- Q5Tại sao khi người dùng truy cập vào ID của người khác mà họ không có quyền, server nên trả về mã lỗi 404 Not Found thay vì 403 Forbidden?
- Q6Trong danh mục bảo mật OWASP API Security Top 10, lỗ hổng IDOR tương ứng với rủi ro số 1 nào?
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Classic SQLi: Vì sao chuỗi biến thành câu lệnh trong Lexer
Bài tiếpThực hành: Audit mã nguồn và HTTP trace phát hiện lỗi sơ cấp
Thảo luận & góp ý
Hỏi đáp về bài này