Phân quyền hỏng đứng đầu OWASP Top 10 — phổ biến hơn cả injection hay XSS. Dạng thường gặp nhất của nó có tên IDOR (Insecure Direct Object Reference): ứng dụng kiểm bạn đã đăng nhập, nhưng quên kiểm bạn có quyền với đúng tài nguyên bạn đang yêu cầu. Kết quả: đổi một con số trong URL là đọc được dữ liệu của người khác. Bài này dựng lại lỗ đó và đo cách chặn.
Toàn bộ chạy trên ứng dụng tự dựng trong container, tự dọn. Chỉ tấn công dữ liệu do tôi tạo. Không nhắm vào hệ thống của ai.
Cuộc tấn công
Ứng dụng có ba đơn hàng, mỗi đơn thuộc một người dùng. an đăng nhập bằng tài khoản của chính mình — hợp lệ — và xem đơn của mình qua /don/1. Rồi an thử đổi số: /don/2. Đơn số 2 thuộc về binh, chứa số thẻ của binh. Câu hỏi duy nhất: ứng dụng có trả về không?
Số đo
Ba lần chạy, kết quả giống hệt:
| Tình huống | Kết quả |
|---|---|
An xem đơn của mình (/don/v0/1) |
trả về — đúng |
An đổi id, xem đơn của Bình (/don/v0/2) |
trả về — lỗ IDOR! |
An đổi id trên route có kiểm quyền (/don/v1/2) |
403 — chặn |
Kẻ tấn công có UUID của đơn người khác (/don/v2) |
trả về — id khó đoán không cứu |
Điều đáng nhớ
"Đã đăng nhập" không phải "được phép". Đây là gốc của mọi IDOR. Route v0 kiểm đúng một điều: người gọi đã đăng nhập. Nó không kiểm người gọi có phải chủ đơn không. Với an đã đăng nhập thật, nó trả về mọi đơn an hỏi tới — kể cả đơn của binh. Xác thực (bạn là ai) và phân quyền (bạn được làm gì) là hai bước tách biệt, và IDOR là khi bước thứ hai bị bỏ quên. Số thẻ của binh rò ra không phải vì mật khẩu yếu hay injection — mà vì một câu if không được viết.
Cách chặn đúng là kiểm quyền sở hữu ở phía server, cho mọi truy cập. Route v1 thêm đúng một dòng: đơn này có chủ là người đang đăng nhập không? Nếu không, 403. Đơn giản đến mức tầm thường, nhưng nó là toàn bộ sự khác biệt giữa an toàn và rò rỉ. Điểm mấu chốt: phép kiểm phải nằm ở server, gắn với danh tính từ phiên (không phải từ tham số client gửi lên), và chạy cho mọi endpoint chạm tới tài nguyên — xem, sửa, xoá, liệt kê.
Id khó đoán là che giấu, không phải phân quyền — và số đo chứng minh điều đó. Dòng cuối bảng: route v2 dùng UUID ngẫu nhiên 32 ký tự thay cho số thứ tự. Nghe có vẻ an toàn — kẻ tấn công không thể đếm 1, 2, 3... để dò. Nhưng UUID vẫn rò ra: qua log máy chủ, qua header Referer, qua lịch sử trình duyệt, qua một link chia sẻ, qua URL dán vào chat. Ngay khi kẻ tấn công có UUID của đơn người khác, v2 trả về nội dung — vì nó không kiểm quyền sở hữu, chỉ dựa vào việc id khó đoán. Đây là security through obscurity: làm khó, không làm bất khả. Một id khó đoán là lớp bảo vệ chiều sâu tốt, nhưng không bao giờ được thay cho phép kiểm quyền.
IDOR không chỉ ở "xem" — và không chỉ ở id số
Ví dụ trong bài là đọc dữ liệu qua id số, nhưng IDOR rộng hơn thế. Cùng lỗ thiếu-kiểm-quyền xuất hiện ở nhiều dạng:
- Ghi, không chỉ đọc.
POST /don/2/huymà không kiểm chủ sở hữu cho phép An huỷ đơn của Bình, không chỉ xem. Hậu quả nặng hơn nhiều. - Id ẩn trong thân request, không chỉ URL. Một form gửi
{"don_id": 2, ...}cũng là IDOR nếu server tindon_idmà không kiểm quyền. Đổi URL hay đổi thân request là như nhau với kẻ tấn công. - Phân quyền cấp trường. Đôi khi bạn được xem một bản ghi nhưng không được xem mọi trường của nó. Một API trả về cả object khi bạn chỉ nên thấy vài trường công khai là một biến thể tinh vi của cùng vấn đề: kiểm quyền ở mức bản ghi nhưng quên ở mức trường.
Điểm chung: ở đâu ứng dụng dùng một định danh do client cung cấp để tra dữ liệu, ở đó phải có một phép kiểm "người này có quyền với định danh này không". Không có ngoại lệ nào an toàn để bỏ qua.
Vì sao
IDOR phổ biến vì nó là thiếu một phép kiểm, không phải có một lỗi — và thiếu sót khó thấy hơn hiện diện:
- Injection, XSS có một payload nhìn thấy được; người rà soát mã có thể tìm chuỗi đáng ngờ. IDOR không có payload — request
/don/2trông y hệt request hợp lệ/don/1. Chỉ khác con số. - Test chức năng thường chạy với một người dùng, xem dữ liệu của họ — và mọi thứ xanh. Lỗ chỉ hiện khi một người dùng chạm tới dữ liệu của người khác, mà kịch bản đó hiếm khi được viết ra.
- Framework tự lo xác thực (middleware đăng nhập), nhưng phân quyền theo tài nguyên thì không tự làm được — nó phụ thuộc logic nghiệp vụ "ai sở hữu cái gì", mà chỉ ứng dụng biết.
Đó là lý do phân quyền hỏng đứng đầu bảng: nó là lỗi của sự vắng mặt, ở đúng chỗ công cụ tự động và test thông thường không nhìn tới.
Nghĩa là gì trong thực tế
- Với mỗi truy cập tài nguyên, hỏi "người này có được chạm vào cái này không" — không chỉ "người này đã đăng nhập chưa". Viết phép kiểm quyền sở hữu ngay tại nơi truy vấn dữ liệu.
- Lấy danh tính từ phiên, không từ request. Nếu ứng dụng nhận
user_idtừ tham số URL rồi tin nó, kẻ tấn công đổi tham số là xong. Chủ sở hữu phải suy ra từ phiên đã xác thực. - Kiểm ở mọi động từ. Một endpoint
GET /don/{id}được bảo vệ nhưngDELETE /don/{id}thì quên là một IDOR đầy đủ. Xem, sửa, xoá, liệt kê — tất cả cần cùng phép kiểm. - Dùng id khó đoán như lớp thêm, không như lớp duy nhất. UUID tốt cho chiều sâu phòng thủ, nhưng phép kiểm quyền mới là lớp chặn thật.
Chỗ tôi không kết luận được
Tôi đo IDOR trên quan hệ sở hữu đơn giản: một đơn thuộc một người. Thực tế phức tạp hơn — quyền theo vai (admin xem được mọi đơn), quyền chia sẻ (chủ đơn mời người khác xem), quyền theo tổ chức (mọi người trong công ty A xem được đơn của A). Mỗi mô hình cần logic phân quyền riêng, và càng phức tạp càng dễ có kẽ hở. Tôi đo cơ chế cốt lõi, không đo các mô hình quyền nâng cao.
Và tôi giả lập phiên bằng một tham số cho gọn. Trong thực tế danh tính đến từ cookie phiên hay token đã ký (các bài trước) — điều quan trọng là nó không đến từ phần request mà kẻ tấn công điều khiển được.
Thử ba mươi giây
Tìm trong ứng dụng của bạn một endpoint nhận id tài nguyên trong URL — /hoa-don/{id}, /tin-nhan/{id}, /ho-so/{id}. Đăng nhập bằng một tài khoản, rồi đổi id sang một tài nguyên bạn biết thuộc về người khác.
Nếu nó trả về dữ liệu, bạn vừa tìm thấy một IDOR — lỗ đứng đầu OWASP Top 10, và là một câu if bị thiếu. Nếu nó trả 403 hoặc 404, kiểm tiếp các động từ khác (sửa, xoá) trên cùng endpoint: phép kiểm phải có ở tất cả, không chỉ ở GET.