Đây là object-level authorization — lớp mà OWASP xếp là lỗ hổng API phổ biến nhất (IDOR/BOLA): endpoint kiểm tra đúng "được xem đơn hàng" nhưng quên kiểm tra "đơn này có phải của bạn không", nên đổi id trên URL là đọc được dữ liệu người khác.
Nguyên tắc: lọc ở tầng truy vấn, không lọc sau khi lấy ra.
-- sai: lấy hết rồi mới lọc trong code
SELECT * FROM orders WHERE id = $1;
-- đúng: điều kiện phạm vi nằm ngay trong WHERE
SELECT * FROM orders WHERE id = $1 AND branch_id = ANY($2);Trả 404 thay vì 403 cho bản ghi ngoài phạm vi để không lộ sự tồn tại của nó.
Ba cách cài đặt, chọn theo mức độ rủi ro:
- Scope trong repository layer: mọi truy vấn đi qua hàm forUser(ctx) tự gắn điều kiện phạm vi. Đơn giản, nhưng vẫn phụ thuộc lập trình viên dùng đúng hàm.
- Policy tập trung: mỗi thực thể có một object policy (can(user, 'view', order) và scopeFor(user)), test riêng được. Đây là mức phù hợp cho đa số hệ thống nghiệp vụ.
- Row-Level Security của Postgres: chính sách nằm trong DB, đúng cả khi có người truy vấn ngoài ứng dụng. Ứng dụng truyền danh tính qua SET LOCAL app.user_id và DB lọc theo policy. Mạnh nhất cho multi-tenant vì một truy vấn quên WHERE tenant_id không còn làm rò dữ liệu chéo tenant.
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_tenant ON orders USING (tenant_id = current_setting('app.tenant_id')::uuid);Lưu ý vận hành: kết nối dùng connection pool nên phải dùng SET LOCAL trong transaction, tránh trạng thái dính sang request khác; và nhớ kiểm tra phạm vi cả ở các đường vòng — export báo cáo, tìm kiếm, endpoint PATCH, chứ không chỉ ở endpoint GET chi tiết.