Bài này thuộc sê-ri phòng thủ / giáo dục. Mọi lỗ hổng được tái hiện trong một lab cô lập (hai server HTTP chạy trong bộ nhớ, dữ liệu giả) với mục đích duy nhất là hiểu cơ chế để biết cách phòng. Đừng thử những kỹ thuật này lên hệ thống bạn không sở hữu hoặc không được phép kiểm thử — đó là hành vi phạm pháp.
Có một hiểu lầm phổ biến: "đăng nhập xong là xong phần bảo mật". Không. Đăng nhập chỉ trả lời câu hỏi "bạn LÀ AI?" (xác thực — authentication). Còn một câu hỏi thứ hai, quan trọng ngang ngửa, mà rất nhiều ứng dụng quên: "bạn ĐƯỢC LÀM GÌ?" (uỷ quyền — authorization). Khoảng trống giữa hai câu hỏi này chính là Broken Access Control — và từ 2021 nó đứng hạng #1 trong OWASP Top 10, vượt cả Injection. Lý do nó phổ biến đến vậy: nó không phải một kỹ thuật tấn công tinh vi, nó là một thứ bị quên. Bài này (phần 11 sê-ri Bảo mật) tái hiện dạng kinh điển nhất của nó — IDOR — trong lab, rồi vá bằng đúng một dòng.
Xác thực ≠ Uỷ quyền
Hai khái niệm hay bị gộp làm một, nhưng chúng tách bạch:
- Xác thực (authentication): xác minh danh tính. Token/session cho server biết request này đến từ
alice. Phần này thường được làm đúng — ai cũng nhớ bắt đăng nhập. - Uỷ quyền (authorization): xác minh quyền hạn.
alicecó được phép xem/sửa/xoá đối tượng cụ thể này không? Phần này hay bị bỏ sót, đặc biệt ở mức từng đối tượng (object-level).
IDOR (Insecure Direct Object Reference) là tên gọi khi uỷ quyền mức đối tượng bị thiếu: ứng dụng nhận một tham chiếu trực tiếp tới đối tượng (thường là id trong URL như /orders/1002) và trả về nó mà không kiểm người gọi có sở hữu đối tượng đó không. Có xác thực đầy đủ, nhưng thiếu uỷ quyền.
// Authentication: "bạn LÀ AI?" -> token cho biết là alice (thường làm đúng)
// Authorization: "bạn ĐƯỢC LÀM GÌ?" -> alice có quyền với order NÀY không? (hay quên)
// IDOR = có xác thực NHƯNG thiếu uỷ quyền ở mức đối tượng

Hình 1: Xác thực (bạn là ai) tách bạch với uỷ quyền (bạn được làm gì). Handler lỗi lấy order theo id rồi trả về mà không kiểm o.Owner == u; handler an toàn thêm đúng một bước kiểm sở hữu trước khi trả dữ liệu.
Tái hiện trong lab: hai server, một dòng khác nhau
Mình dựng hai HTTP server trong go-lab, giống hệt nhau trừ một chỗ. Có hai user (alice, bob), mỗi người một đơn hàng; token trong header Authorization cho biết người gọi là ai (phần xác thực — cả hai server đều có).
Handler lỗ hổng chỉ có xác thực:
func vulnerable(w http.ResponseWriter, r *http.Request) {
u := currentUser(r) // xác thực: biết là alice
if u == "" {
http.Error(w, "chua dang nhap", http.StatusUnauthorized)
return
}
id := r.URL.Query().Get("id")
o, ok := orders[id]
if !ok {
http.Error(w, "khong thay", http.StatusNotFound)
return
}
// THIẾU: không kiểm o.Owner == u
fmt.Fprintf(w, "Don hang cua %s: %s", o.Owner, o.Data)
}
Handler an toàn chỉ thêm một dòng — kiểm sở hữu:
func secure(w http.ResponseWriter, r *http.Request) {
u := currentUser(r)
if u == "" {
http.Error(w, "chua dang nhap", http.StatusUnauthorized)
return
}
id := r.URL.Query().Get("id")
o, ok := orders[id]
if !ok || o.Owner != u { // KIỂM QUYỀN: không phải chủ -> 403
http.Error(w, "khong co quyen", http.StatusForbidden)
return
}
fmt.Fprintf(w, "Don hang cua %s: %s", o.Owner, o.Data)
}
Rồi mình đóng vai Alice (đã đăng nhập hợp lệ), gọi hai lần vào mỗi server: một lần với id đơn hàng của chính mình (1001), một lần đổi id sang đơn của Bob (1002).

Hình 2: Kết quả đo thật. Server lỗ hổng: Alice đổi id sang 1002 vẫn nhận HTTP 200 kèm đơn hàng nhạy cảm của Bob. Server an toàn: cùng request đó trả HTTP 403 "khong co quyen". Khác biệt duy nhất giữa hai server là một dòng kiểm o.Owner != u.
Đọc kết quả thật:
- Server lỗ hổng (IDOR): Alice đọc đơn của mình (1001) →
HTTP 200, trảDon hang cua Alice: 2 trieu— đúng và hợp lệ. Nhưng khi Alice đổi id trong URL sang 1002 → vẫnHTTP 200, trảDon hang cua Bob: 50 trieu (nhay cam). Alice đọc được dữ liệu của Bob chỉ bằng cách sửa một con số trên URL. Không cần công cụ hacking, không cần vượt tường lửa — gõ số khác là xong. Đây chính là IDOR. - Server an toàn: cùng hai request. Đọc đơn của mình →
HTTP 200. Nhưng đổi id sang 1002 →HTTP 403 "khong co quyen". Server kiểmo.Owner != u, thấy đơn 1002 thuộc Bob chứ không phải Alice, nên từ chối. Dữ liệu của Bob được bảo vệ.
Điều đáng sợ của IDOR nằm ở sự tầm thường của nó: lỗ hổng không phải ở thuật toán mã hoá hay cấu hình phức tạp, mà ở một dòng kiểm bị quên. Và cách khai thác đơn giản tới mức một người dùng bình thường có thể vấp phải tình cờ khi nghịch URL.
Vì sao phải kiểm ở SERVER, mỗi request
Một cái bẫy tư duy hay gặp: "mình đã ẩn nút Xem đơn của người khác trên giao diện rồi, người dùng đâu bấm được". Sai hoàn toàn. Giao diện (client) không phải ranh giới bảo mật. Client chạy trên máy của người dùng — họ toàn quyền sửa HTML, gọi thẳng API bằng curl, hay chỉ đơn giản là gõ URL khác vào thanh địa chỉ. Mọi quyết định uỷ quyền phải nằm ở server, nơi người dùng không can thiệp được.
Và phải kiểm mỗi request, trên mỗi đối tượng được truy cập — không phải một lần lúc đăng nhập. Xác thực xảy ra một lần (lúc đăng nhập, cấp token); uỷ quyền phải xảy ra mỗi lần chạm vào một tài nguyên, vì mỗi tài nguyên có chủ sở hữu/quyền khác nhau.
Một mẹo kiến trúc rất hiệu quả: đẩy điều kiện quyền xuống tận câu truy vấn. Thay vì SELECT * FROM orders WHERE id = ? rồi mới kiểm chủ trong code (dễ quên), viết thẳng SELECT * FROM orders WHERE id = ? AND owner = ? với tham số thứ hai là người dùng hiện tại. Khi đó, đơn không thuộc về họ sẽ không có trong kết quả ngay từ đầu — không có đối tượng để lỡ tay trả về. Điều kiện quyền trở thành một phần của phép lấy dữ liệu, không phải một bước kiểm có thể bị bỏ sót.
Đánh đổi và những góc khuất
ID khó đoán KHÔNG phải uỷ quyền. Một phản xạ sai là đổi id tuần tự (1001, 1002…) thành UUID ngẫu nhiên để "không ai đoán được id của người khác". Đó là security through obscurity — nó làm khó việc dò id, nhưng không phải kiểm quyền. id vẫn lộ ra ở muôn nơi: URL chia sẻ, log, lịch sử trình duyệt, Referer header. Dùng UUID là tốt (giảm bề mặt dò quét), nhưng nó bổ sung chứ không thay thế cho kiểm sở hữu phía server.
Access control phức tạp hơn "chủ sở hữu". Lab này dùng mô hình đơn giản nhất: một đối tượng, một chủ. Thực tế có vai trò (admin xem được mọi đơn), chia sẻ (Bob cho Alice quyền xem đơn của mình), phân cấp (quản lý xem được đơn của nhân viên dưới quyền). Khi đó logic uỷ quyền nên tách ra một tầng riêng (policy/RBAC/ABAC) thay vì rải if o.Owner != u khắp nơi — rải rác là công thức để quên một chỗ. Nhưng nguyên tắc không đổi: server quyết định, mỗi request.
Đừng quên các thao tác GHI. IDOR hay được minh hoạ bằng đọc (như lab này), nhưng nguy hiểm nhất thường là ghi: POST /orders/1002/cancel hay DELETE /users/1002 mà không kiểm quyền cho phép Alice huỷ đơn hoặc xoá tài khoản của Bob. Mọi endpoint đụng tới một đối tượng — đọc, sửa, xoá — đều cần cùng một lớp kiểm.
Ba ý mang về
- Broken Access Control đứng #1 OWASP vì nó là thứ bị quên, không phải thứ khó. Xác thực (bạn là ai) được làm đúng gần như mọi nơi; uỷ quyền mức đối tượng (bạn có quyền với cái này không) hay bị bỏ sót. Đo thật trong lab: server chỉ xác thực để Alice đổi
idtrên URL và đọc đơn 50 triệu của Bob vớiHTTP 200. - Bản vá là một dòng, nhưng phải đặt đúng chỗ: ở SERVER, mỗi request, mỗi đối tượng. Thêm
if o.Owner != u { return 403 }biến cùng request IDOR đó thànhHTTP 403. Tốt hơn nữa: đẩy điều kiện quyền xuống truy vấn (WHERE id = ? AND owner = ?) để không còn đối tượng nào để lỡ tay trả về. - Client không phải ranh giới bảo mật, và id khó đoán không phải uỷ quyền. Ẩn nút trên giao diện hay dùng UUID chỉ làm khó, không chặn — người dùng sửa URL/gọi thẳng API được. Và nhớ áp lớp kiểm cho cả thao tác ghi (sửa/xoá), không chỉ đọc.
Nguồn
- OWASP Top 10:2021 — A01 Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- OWASP — Insecure Direct Object Reference Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html
- OWASP — Authorization Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
Phần sau là bài tổng kết sê-ri: ánh xạ 11 bài đã học vào OWASP Top 10, một checklist bảo mật để chạy trước khi lên production, và cây quyết định review code dưới góc nhìn an ninh.