Bỏ qua để đến nội dung
Knowledge Hub

BOLA/IDOR: đăng nhập rồi vẫn đọc được đơn của người khác Cơ bản

Thời gian đọc ước tính: 8 phút

User A mở /orders/order-42 và thấy đơn của mình. User B đã đăng nhập, có được order-42 từ một link hoặc email, rồi gọi cùng endpoint. API chỉ kiểm tra token hợp lệ và chạy SELECT * FROM orders WHERE id = $1. Đơn của A trả về cho B.

Xác thực không sai: B là người thật. Câu hỏi bị bỏ qua là B có quyền với chính object order-42 không? OWASP gọi lớp lỗi này là BOLA (Broken Object Level Authorization, API1:2023, đứng đầu API Security Top 10). IDOR là tên cũ hơn, hay dùng cho cùng ý tưởng.

Từ ID client gửi đến quyết định cho phép đọc object
Từ ID client gửi đến quyết định cho phép đọc objectBước 1/4: B gửi GET /orders/order-42. Token hợp lệ, nhưng ID trong URL là input không đáng tin.User Bđã đăng nhậpAPIordersorder-42 của Aorder-42
User B → APIorder-42
Bước 1/4

B gửi GET /orders/order-42. Token hợp lệ, nhưng ID trong URL là input không đáng tin.

Khác biệt nằm ở nguồn của điều kiện lọc. tenant_id và owner_id phải đến từ identity đã xác thực ở server, không từ query hay body.

// SAI: có ID là đọc được
const order = await db.query('SELECT * FROM orders WHERE id = $1', [id]);
return order.rows[0];
// ĐÚNG: scope theo actor ngay trong query; actor lấy từ token đã xác minh
const { rows } = await db.query(
'SELECT * FROM orders WHERE id = $1 AND tenant_id = $2 AND owner_id = $3',
[id, actor.tenantId, actor.userId],
);
if (rows.length === 0) throw new NotFound(); // không phân biệt "không tồn tại" và "không phải của bạn"
return rows[0];

Với update/delete, đặt predicate quyền vào chính câu lệnh rồi kiểm tra số row bị ảnh hưởng:

UPDATE orders SET status = 'cancelled'
WHERE id = $1 AND tenant_id = $2 AND owner_id = $3 AND status = 'pending'
RETURNING id;

Đặt check và thao tác trong cùng một statement còn loại được race kiểu “kiểm tra owner xong, owner đổi, rồi mới ghi”.

Cả ba cùng thuộc nhóm “đã xác thực nhưng làm quá quyền”, khác nhau ở mức kiểm tra bị thiếu. Số thứ tự theo OWASP API Security Top 10 2023:

Mã Tên Câu hỏi bị bỏ qua Ví dụ
API1 BOLA: object level Người gọi có quyền với object này không? User B đọc /orders/order-42 của A
API3 BOPLA: object property level Người gọi được đọc/ghi thuộc tính này của object không? User thường gửi PATCH {"role":"admin"} (mass assignment), hoặc response trả thừa costPrice
API5 BFLA: function level Người gọi có được gọi hành động này không? User thường gọi DELETE /admin/users/7

Cách nhớ: BFLA hỏi “được dùng chức năng này không”, BOLA hỏi “được đụng vào bản ghi này không”, BOPLA hỏi “được thấy hoặc sửa trường này không”. Bài này tập trung vào BOLA; sửa BOPLA bằng DTO/allowlist trường cho cả input và output, sửa BFLA bằng kiểm tra role/permission theo từng route.

Quyết định cho phép: subject, action, object

Phần tiêu đề “Quyết định cho phép: subject, action, object”

Authentication trả lời “bạn là ai”. Authorization trả lời “bạn được làm hành động này trên object này không”. Với mỗi ID từ client, server cần subject, action, object và context (tenant, trạng thái object).

Policy trong demo (cùng tenant và là owner) chỉ là ví dụ. Hệ thống thật có thể cho nhân viên hỗ trợ xem đơn trong tenant qua role có audit, hay cho người nhận xem một phần dữ liệu. Viết policy rõ theo nghiệp vụ và test cho từng vai.

Hai cách triển khai:

Cách Khi nào Lưu ý
Scope query (WHERE ... AND owner_id) Mặc định cho policy “thuộc về ai” Dữ liệu ngoài phạm vi không bao giờ rời database
Load rồi gọi policy Policy phức tạp (role, chia sẻ, delegation) Không serialize, log, cache hoặc gửi downstream trước khi check

404 hay 403: trả 404 khi không muốn lộ object tồn tại; 403 khi người gọi được biết object tồn tại nhưng không được làm action. Chọn một quy ước và giữ nhất quán. Thông điệp lỗi và thời gian phản hồi cũng không được phân biệt hai trường hợp.

BOLA hiếm khi nằm ở route detail đã được review kỹ. Nó nằm ở các đường còn lại:

Endpoint Cách lỗi xảy ra Cách chặn
List GET /orders Chỉ detail được scope, list trả đơn cả tenant Scope list theo actor; test với 2 tenant
Nested /users/A/orders/order-42 Check A có tồn tại nhưng không check order thuộc A hay actor được xem Join hoặc predicate theo cả hai ID và theo actor
Bulk DELETE /orders với mảng ID Chỉ check phần tử đầu Check từng ID, hoặc WHERE id = ANY($1) AND owner_id = $2 rồi so số row với số ID
Export / download Route CSV hoặc file bỏ qua check của API chính Dùng chung hàm scope; signed URL giới hạn object và thời gian
Job nền, GraphQL resolver, websocket Nhận ID từ message, không có actor Truyền actor vào job, check tại resolver và handler

Row Level Security của PostgreSQL là lớp bảo vệ bổ sung ở database: policy tự thêm điều kiện cho mọi query. Cần set identity vào session đúng cách, đặc biệt với connection pool, và test các role, job. RLS không thay việc định nghĩa action và response ở API.

Terminal window
npx --prefix examples ts-node examples/src/bola/bola-demo.ts

Hàm sai unsafeGetOrder(id) trả order-42 cho user-b; hàm đúng getOrder(actor, id) trả order cho user-a và not found cho user-b. Demo dùng Map trong RAM, không có HTTP hay SQL; nó minh họa quyết định quyền. Muốn chứng minh API thật được bảo vệ, cần integration test qua endpoint.

  1. Tạo ít nhất hai user, hai tenant, mỗi bên có object riêng.
  2. Gọi endpoint bằng token của owner: được phép.
  3. Gọi cùng ID bằng token user khác cùng tenant, và bằng user tenant khác: không nhận data, response không lộ owner hay tenant.
  4. Lặp lại cho list, update, delete, bulk, export và nested route.
  5. Review code: tìm findById, findUnique, path param hoặc file ID đến từ request và lần theo tới nơi check quyền.

Test qua endpoint thật tốt hơn unit test policy, vì một route mới có thể quên gọi policy hoàn toàn.

  1. Mọi truy cập object theo ID từ client đều đi qua scope query hoặc policy check, ở cả read, update, delete.
  2. userId, tenantId, roles lấy từ token hoặc session đã xác minh, không từ body hay query.
  3. List, nested, bulk, export, download, job nền, GraphQL và websocket đều dùng cùng hàm scope.
  4. Update/delete có predicate quyền trong statement và kiểm tra số row ảnh hưởng.
  5. 404/403 nhất quán, thông điệp lỗi không lộ sự tồn tại hay owner.
  6. Có integration test với tối thiểu hai tenant và hai user cho từng endpoint mới.
  7. Log từ chối quyền kèm actor và object ID; cảnh báo khi một actor bị từ chối hàng loạt (dấu hiệu quét ID).
  8. Dùng UUID để giảm đoán ID, nhưng không coi đó là kiểm soát truy cập.

Check ở object level

  • Ngăn người đã đăng nhập vượt sang object của người khác hoặc tenant khác.
  • Predicate theo actor đẩy xuống query, không load dữ liệu ngoài phạm vi.
  • Policy rõ cho phép test theo vai và action.

Điểm cần thiết kế

  • Policy nhiều role, chia sẻ, delegation cần model dễ review.
  • Scope query rải rác dễ bị quên; cần repository hoặc abstraction dùng chung.
  • RLS thêm chi phí vận hành (session identity, pool, debug).

Tự kiểm tra: (1) Vì sao requireLogin không đủ cho GET /orders/:id? (2) PATCH {"role":"admin"} thuộc BOLA, BOPLA hay BFLA? (3) Bulk delete cần check những gì?