Chương 6: Access Control — Kiểm soát truy cập
Khái niệm
Access Control (Kiểm soát truy cập) là cơ chế quyết định ai được làm gì trong hệ thống. Đây là lỗ hổng số 1 trong OWASP Top 10 2021 (A01: Broken Access Control).
Mức độ nguy hiểm: Rất cao — Broken access control cho phép hacker truy cập dữ liệu/chức năng không thuộc về họ.
Ba thành phần liên kết:
- Authentication: Bạn là ai?
- Session Management: Theo dõi bạn qua các requests
- Access Control: Bạn được phép làm gì?
Phân loại Access Control
Vertical Access Control (Phân quyền dọc)
Giới hạn chức năng theo role (vai trò):
Admin → tất cả chức năng
Manager → quản lý user, xem reports
User → chỉ profile của mình
Guest → xem public content
Lỗ hổng: Vertical Privilege Escalation — user thường truy cập được chức năng admin.
Horizontal Access Control (Phân quyền ngang)
Giới hạn truy cập vào dữ liệu theo owner:
User A → chỉ xem account của User A
User B → chỉ xem account của User B
Lỗ hổng: Horizontal Privilege Escalation (IDOR) — User A xem được dữ liệu của User B.
Context-Dependent Access Control
Giới hạn theo trạng thái/ngữ cảnh:
Không cho phép sửa cart sau khi đã thanh toán
Không cho phép hủy order sau khi đã ship
IDOR — Insecure Direct Object Reference
IDOR (Insecure Direct Object Reference — Tham chiếu Đối tượng trực tiếp không an toàn) là lỗ hổng phổ biến nhất trong access control.
Xảy ra khi app dùng user-supplied input để truy cập object trực tiếp mà không kiểm tra quyền.
Ví dụ IDOR cơ bản
GET /api/users/profile?userId=1337 HTTP/1.1
Authorization: Bearer <token_of_user_1337>
Response: {"id": 1337, "name": "Alice", "email": "alice@example.com"}
Hacker thay đổi userId:
GET /api/users/profile?userId=1 HTTP/1.1
Authorization: Bearer <token_of_user_1337>
Response: {"id": 1, "name": "Admin", "email": "admin@example.com"}
Không có kiểm tra "user 1337 có quyền xem profile của user 1 không?" → IDOR.
IDOR trong các dạng khác
IDOR trong path:
GET /api/orders/ORD-001234
→ thử: GET /api/orders/ORD-000001
IDOR trong POST body:
POST /api/update-profile
{"userId": 1337, "email": "new@email.com"}
→ thay: {"userId": 1, "email": "hacked@email.com"}
IDOR trong file download:
GET /download?file=invoice_1337.pdf
→ thử: GET /download?file=invoice_1.pdf
IDOR qua Indirect Reference:
GET /api/messages?thread=abc123
→ Nếu thread ID predictable hoặc enumerable
IDOR với UUID — Không phải silver bullet
UUID: 550e8400-e29b-41d4-a716-446655440000
→ Random, không đoán được
→ Nhưng nếu server vẫn không check ownership → vẫn IDOR!
Test: Chia sẻ UUID của resource của bạn với account khác
→ Nếu account khác truy cập được → IDOR dù dùng UUID
Vertical Privilege Escalation
Unprotected Admin Functions
Admin panel tại: /admin
→ Không có authentication check
→ Chỉ "ẩn" khỏi navigation menu
→ Hacker biết URL → access ngay
Tìm hidden endpoints:
# Dùng feroxbuster/ffuf để brute force paths
ffuf -w /usr/share/wordlists/dirb/common.txt \
-u https://example.com/FUZZ \
-mc 200,301,302,403
# Xem robots.txt - thường tiết lộ admin paths
curl https://example.com/robots.txt
# Xem JavaScript source - thường chứa API endpoints
grep -r "admin\|dashboard\|internal" static/js/
Parameter Tampering
GET /account?role=user HTTP/1.1
→ Thử: GET /account?role=admin HTTP/1.1
POST /login HTTP/1.1
{"username": "alice", "password": "pass123"}
Response: {"userId": 1337, "role": "user", "token": "..."}
→ Nếu app tin vào role trong response mà không verify server-side
→ Sửa role=admin trong subsequent requests
Platform Misconfiguration
Một số framework cho phép restrict theo HTTP method:
// Sai: restrict GET nhưng không restrict POST
@GetMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public List<User> getUsers() { ... }
// Admin function cũng accessible qua:
POST /admin/users ← không có annotation → kh ông bị restrict!
URL path override:
GET /admin/users HTTP/1.1
→ 403 Forbidden
GET /users HTTP/1.1
X-Original-URL: /admin/users ← một số framework override path
→ 200 OK → access bypassed!
Broken Access Control qua HTTP Method Override
POST /api/delete-user HTTP/1.1
X-HTTP-Method-Override: DELETE
→ Một số servers map POST + X-HTTP-Method-Override → DELETE
→ Bypass firewall rules chỉ block DELETE
Kịch bản tấn công: IDOR Mass Account Takeover
Target: e-commerce site với 100,000 accounts
1. Login với account hacker (ID: 50000)
2. Request profile: GET /api/profile?id=50000
3. Thay id: GET /api/profile?id=50001 → thành công!
4. Viết script:
for id in range(1, 100001):
response = requests.get(f"/api/profile?id={id}",
cookies={"session": hacker_session})
save_profile_data(response.json())
5. Dump toàn bộ 100,000 profiles bao gồm email, phone, address
6. Bán data breach hoặc dùng để phishing
Cách phát hiện
□ Thay đổi ID trong URL/parameters → truy cập được resource người khác?
□ Xóa/thay đổi Authorization header → vẫn có response?
□ Thay đổi role/admin parameter → có thay đổi quyền không?
□ Access admin URL trực tiếp khi logged in với user thường
□ Thử methods khác (POST/PUT/DELETE) trên endpoint chỉ allow GET
□ Thêm X-Original-URL, X-Rewrite-URL, X-Forwarded-For headers
□ Thử path với trailing slash, uppercase, encoded chars
□ Robots.txt, sitemap.xml có tiết lộ hidden paths không?
□ JavaScript source có chứa admin endpoints không?
Tool hữu ích:
- Autorize (Burp extension): Tự động replay mọi request với session của user khác để detect IDOR
- 403 Bypasser (Burp extension): Tự động thử bypass 403
Phòng chống
1. Server-Side Authorization Check
# SAI: tin vào user-supplied ID
@app.route('/api/profile')
def get_profile():
user_id = request.args.get('userId') # Từ client
return db.get_user(user_id)
# ĐÚNG: dùng ID từ session (server-controlled)
@app.route('/api/profile')
@login_required
def get_profile():
user_id = g.current_user.id # Từ server-side session
return db.get_user(user_id)
# ĐÚNG: nếu cần user specify ID, luôn check ownership
@app.route('/api/orders/<int:order_id>')
@login_required
def get_order(order_id):
order = db.get_order(order_id)
if order.user_id != g.current_user.id:
abort(403) # Forbidden
return order
2. Deny by Default
# Middleware check access cho mọi request
def check_access(resource_type, resource_id, action):
user = g.current_user
# Default: deny
if not has_permission(user, resource_type, resource_id, action):
abort(403)
# Áp dụng cho tất cả routes
@app.before_request
def enforce_access_control():
if is_protected_endpoint(request.endpoint):
resource = get_resource_from_request()
check_access(resource.type, resource.id, request.method)
3. RBAC (Role-Based Access Control)
PERMISSIONS = {
'admin': ['users:read', 'users:write', 'orders:read', 'orders:write'],
'manager': ['orders:read', 'orders:write'],
'user': ['profile:read', 'profile:write', 'orders:read'],
}
def has_permission(user, permission):
return permission in PERMISSIONS.get(user.role, [])
@app.route('/api/users')
@require_permission('users:read')
def list_users():
return db.get_all_users()
4. Indirect Reference Maps
Thay vì expose database ID trực tiếp, dùng một mapping:
# Mỗi user có mapping riêng của họ
user_resource_map = {
'order_1': 'db_order_id_1337',
'order_2': 'db_order_id_1338',
}
# User chỉ thấy reference key của riêng họ
GET /api/orders/order_1 → mapped to actual order 1337 của user đó
Góc nhìn DevOps
Kubernetes RBAC — không liên quan đến Web RBAC nhưng tư duy tương tự:
# Least privilege: chỉ đọc pods trong namespace cụ thể
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
# Không có "create", "delete", "update"
API Gateway — Enforce access control ở gateway level:
# Kong Gateway: restrict endpoint theo role
plugins:
- name: acl
config:
allow:
- admin-group
hide_groups_header: true
Audit Logging — Ghi lại ai truy cập gì:
# Mọi access vào sensitive resource đều cần log
def audit_log(user, action, resource, status):
logger.info({
"timestamp": datetime.utcnow().isoformat(),
"user_id": user.id,
"username": user.email,
"action": action,
"resource": resource,
"status": status,
"ip": request.remote_addr,
"user_agent": request.user_agent.string
})
# Alert khi có access pattern bất thường
# VD: 1 user request 1000 profiles trong 60 giây → IDOR attempt
CI/CD Security Gate:
# GitHub Actions: chạy DAST test để detect IDOR
- name: Run IDOR Tests
run: |
# Test với hai accounts khác nhau
python tests/security/test_access_control.py \
--user1-token $USER1_TOKEN \
--user2-token $USER2_TOKEN
Tóm tắt
- Broken Access Control là lỗ hổng #1 OWASP — phổ biến và nguy hiểm.
- IDOR: user truy cập resource của user khác bằng cách thay ID — kiểm tra ownership mọi lúc.
- Vertical escalation: user thường truy cập chức năng admin.
- Luôn implement access control ở server-side, không tin client.
- Deny by default: nếu không có permission explicit → từ chối.
- Dùng RBAC để quản lý quyền có hệ thống.
- Audit log mọi access vào sensitive resources.
- Dùng Autorize extension để test access control tự động.
Câu hỏi ôn tập
- IDOR là gì và tại sao dùng UUID thay vì integer ID không tự động phòng chống IDOR?
- Sự khác biệt giữa horizontal và vertical privilege escalation là gì?
- Tại sao "deny by default" là nguyên tắc quan trọng hơn "allow by default"?
- Làm thế nào để dùng Burp Suite Autorize để test access control tự động?
- Mô tả cách implement RBAC đơn giản trong một REST API.