Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang test một nền tảng SaaS quản lý nhân sự cho các doanh nghiệp Việt Nam. Công ty A là một chuỗi cà phê 500 nhân viên, công ty B là một startup fintech 30 người. Cả hai cùng dùng chung một hệ thống, chung một database, chung một API — chỉ khác nhau ở tenant_id. Bây giờ, nếu một kế toán của công ty A gõ đúng URL và vô tình xem được bảng lương của công ty B, đó không còn là một "bug nhỏ" nữa. Đó là một sự cố rò rỉ dữ liệu (data leak) có thể khiến khách hàng huỷ hợp đồng, kiện tụng, và làm sụp uy tín của cả sản phẩm.
Đây chính là lý do bài học này tồn tại. Multi-tenancy — kiến trúc cho phép nhiều khách hàng (tenant) dùng chung một hệ thống nhưng dữ liệu phải tách biệt tuyệt đối — là mô hình phổ biến nhất của mọi sản phẩm SaaS ngày nay, từ Base.vn, Misa, KiotViet cho tới Slack hay Notion. Và điểm yếu chết người của nó luôn nằm ở tầng authorization (phân quyền): hệ thống có thực sự chặn được việc user tenant A truy cập tài nguyên của tenant B hay không?
Là một API Tester, bạn không thể chỉ test "happy path" — tức là user đăng nhập đúng, xem đúng data của mình. Bạn phải chủ động đóng vai kẻ tấn công, cố tình gõ nhầm ID, cố tình mượn token của người khác, cố tình gọi endpoint mà mình không có quyền. Trong bài này, chúng ta sẽ xây dựng một test matrix (ma trận kiểm thử) có hệ thống cho multi-tenant authorization, và học cách tự động hoá toàn bộ trong Postman. Đây là loại kỹ năng phân biệt một tester "click nút" với một tester được các công ty trả lương cao.
Khái niệm cốt lõi
Multi-tenant nghĩa là gì với người test API
Trong một hệ thống multi-tenant, mỗi request thường mang theo hai lớp thông tin định danh: danh tính người dùng (ai đang gọi — thường nằm trong JWT/token) và phạm vi tenant (dữ liệu thuộc tổ chức nào). Server phải kiểm tra: token này có hợp lệ không, VÀ tài nguyên đang bị yêu cầu có thuộc về tenant của token đó không. Lỗ hổng xảy ra khi server chỉ kiểm tra vế đầu ("token hợp lệ") mà quên vế sau ("tài nguyên có đúng tenant không").
Có ba mô hình cô lập dữ liệu bạn cần biết để test đúng:
- Database riêng cho mỗi tenant: mỗi khách hàng một database. Cô lập mạnh nhất nhưng đắt. Lỗi ở đây thường là routing sai database.
- Schema riêng trong chung database: mỗi tenant một schema. Lỗi thường là query nhầm schema.
- Chung bảng, phân biệt bằng cột
tenant_id(shared table): phổ biến nhất vì rẻ, nhưng cũng rủi ro nhất — chỉ cần một câu query quênWHERE tenant_id = ?là toàn bộ dữ liệu khách hàng bị trộn lẫn.
Hai loại lỗ hổng phân quyền kinh điển
Có hai thuật ngữ bạn phải nắm rõ vì chúng xuất hiện trong mọi báo cáo bảo mật:
IDOR (Insecure Direct Object Reference) — tham chiếu đối tượng không an toàn. Đây là khi bạn có thể truy cập tài nguyên của người khác chỉ bằng cách đổi ID trong request. Ví dụ: GET /api/invoices/1001 trả về hoá đơn của bạn, nhưng thử GET /api/invoices/1002 lại trả về hoá đơn của tenant khác. Đây là loại lỗi theo chiều ngang (horizontal): bạn và nạn nhân cùng cấp quyền, chỉ khác tenant/khác người.
Privilege Escalation (leo thang đặc quyền) — khi một user cấp thấp thực hiện được hành động của user cấp cao. Ví dụ: một nhân viên thường gọi được endpoint DELETE /api/users/55 vốn chỉ dành cho admin. Đây là lỗi theo chiều dọc (vertical): cùng tenant nhưng vượt cấp vai trò.
Test multi-tenant chính là test giao thoa của cả hai: chiều ngang (cross-tenant) và chiều dọc (cross-role).
Test matrix — xương sống của việc test authorization
Cách chuyên nghiệp để không bỏ sót là dựng một ma trận kiểm thử. Bạn liệt kê các "actor" (người thực hiện) trên một trục, và các "tài nguyên/hành động" trên trục kia, rồi điền kết quả mong đợi vào từng ô. Ví dụ với một tài nguyên hoá đơn:
| Actor gọi request | Tài nguyên | Kết quả mong đợi |
|---|---|---|
| User tenant A, role admin | Hoá đơn của tenant A | 200 OK |
| User tenant A, role nhân viên | Hoá đơn của tenant A | 200 OK (nếu có quyền xem) |
| User tenant A | Hoá đơn của tenant B | 403 Forbidden hoặc 404 Not Found |
| User tenant A, role nhân viên | DELETE hoá đơn tenant A | 403 Forbidden |
| Không có token | Bất kỳ hoá đơn nào | 401 Unauthorized |
| Token đã hết hạn/thu hồi | Hoá đơn của chính mình | 401 Unauthorized |
Tình huống thực tế
Tình huống 1: KiotViet-style POS và hoá đơn xuyên cửa hàng
Giả sử một công ty phần mềm bán hàng ở TP.HCM (gọi là "SmartPOS") phục vụ 8.000 cửa hàng, dùng mô hình shared-table với cột shop_id. Đội QA nhận yêu cầu test tính năng "xem lịch sử hoá đơn". Ban đầu họ chỉ test: chủ shop đăng nhập, xem được hoá đơn của shop mình — pass, ship lên production.
Ba tuần sau, một khách hàng phản ánh: nhân viên cũ đã nghỉ việc vẫn xem được doanh thu cửa hàng cũ qua app. Điều tra ra, endpoint GET /api/v2/invoices?shop_id=1234 lấy shop_id trực tiếp từ query param do client gửi lên, chứ không lấy từ token. Nghĩa là bất kỳ ai cũng chỉ cần đổi số shop_id là xem được hoá đơn shop khác.
Bài học rút ra: không bao giờ tin tenant_id do client gửi. Là tester, bạn phải chủ động test bằng cách đăng nhập với token của shop 1234 nhưng gửi request kèm shop_id=5678. Nếu kết quả trả về là 200 với data shop 5678, bạn vừa tìm ra một lỗ hổng nghiêm trọng đáng giá cả hợp đồng khách hàng. Đội QA đáng lẽ nên có sẵn dòng test này trong matrix ngay từ đầu.
Tình huống 2: SaaS nhân sự và IDOR trên đường dẫn URL
Một startup HR-tech ở Hà Nội (gọi là "HrFlow") có API GET /api/employees/{id}/salary. Họ có kiểm tra token hợp lệ, và có kiểm tra role (chỉ HR mới xem lương). Nhưng họ quên kiểm tra: nhân viên {id} này có cùng công ty với người gọi hay không.
Kết quả: một HR của công ty A (tenant A) có token hợp lệ, có đúng role HR, gọi GET /api/employees/9002/salary — và nhân viên 9002 lại thuộc công ty B. Server thấy "token ok, role HR ok" nên trả về lương. Đây là IDOR kết hợp: kiểm tra đúng chiều dọc (role) nhưng sai chiều ngang (tenant).
Đội QA phát hiện ra nhờ một test-case rất đơn giản trong Postman: dùng token HR của tenant A, lặp qua một danh sách employee ID thuộc tenant B, và assert rằng mọi response phải là 403/404. Chỉ cần một cái 200 lọt lưới là fail cả test suite. Bài học: role đúng không có nghĩa là tenant đúng — phải test cả hai trục độc lập.
Tình huống 3: Fintech và token bị "mượn" giữa hai môi trường
Một công ty ví điện tử (gọi là "PayNow") có nhiều tenant là các ngân hàng đối tác. Trong quá trình test, một QA phát hiện rằng token cấp cho tenant "Bank X" ở môi trường staging vẫn được chấp nhận khi gọi API dữ liệu của tenant "Bank Y", miễn là chữ ký JWT còn hợp lệ. Nguyên nhân: backend chỉ verify chữ ký và thời hạn (exp) của JWT, nhưng không đối chiếu claim tenant_id bên trong token với tenant của tài nguyên.
QA đã dựng một test có chủ đích: giải mã payload JWT (phần giữa của token, mã hoá base64) để đọc tenant_id, sau đó cố tình gọi tài nguyên của tenant khác. Việc này bắt được lỗ hổng trước khi lên production. Bài học: một JWT hợp lệ về mặt chữ ký không đồng nghĩa với việc nó được phép truy cập mọi tenant — test phải soi vào nội dung claim, không chỉ soi trạng thái "token còn sống hay chết".
Hướng dẫn từng bước
Bây giờ ta biến lý thuyết thành một bộ test chạy được trong Postman. Ý tưởng: chuẩn bị token của nhiều actor khác nhau, rồi cho từng actor "tấn công" tài nguyên chéo tenant, và assert kết quả bị chặn.
Bước 1 — Chuẩn bị dữ liệu và token cho từng actor. Trong Environment (xem lại Bài 5), tạo các biến giữ token và ID mẫu của mỗi tenant. Đừng hard-code ID; hãy lưu chúng thành biến để tái sử dụng:
tenantA_token = {{token của user tenant A}}
tenantB_token = {{token của user tenant B}}
tenantB_invoiceId = 1002 // hoá đơn hợp lệ của tenant B, dùng làm "mồi"
employee_role_token = {{token role nhân viên thường của tenant A}}
Bước 2 — Viết request "tấn công chéo tenant". Tạo một request lấy tài nguyên của tenant B nhưng đính token của tenant A vào header. Trong tab Authorization hoặc Headers:
GET {{baseUrl}}/api/invoices/{{tenantB_invoiceId}}
Authorization: Bearer {{tenantA_token}}
Bước 3 — Viết Tests script assert bị chặn (kiến thức từ Bài 8). Đây là trái tim của test — nó khẳng định rằng cross-tenant access bị từ chối, và quan trọng hơn, dữ liệu tenant B không lọt ra:
pm.test("Cross-tenant bị chặn (403 hoặc 404)", function () {
pm.expect(pm.response.code).to.be.oneOf([403, 404]);
});pm.test("Không rò rỉ dữ liệu tenant B", function () {
// Dù status là gì, body tuyệt đối không được chứa data thật
const body = pm.response.text();
pm.expect(body).to.not.include("companyB_secret_field");
});
Bước 4 — Test IDOR bằng vòng lặp qua danh sách ID. Với data-driven testing (xem Bài 15), nạp một file CSV chứa các ID thuộc tenant khác, rồi để Postman/Newman lặp qua từng dòng và assert tất cả đều bị chặn:
// Với mỗi iteration, request dùng {{victimId}} từ CSV, token của mình
pm.test(ID ${pm.iterationData.get("victimId")} phải bị chặn, function () {
pm.expect(pm.response.code).to.be.oneOf([403, 404]);
});
Bước 5 — Test privilege escalation (chiều dọc). Dùng token role nhân viên thường để gọi một endpoint admin-only, ví dụ xoá người dùng:
DELETE {{baseUrl}}/api/users/55
Authorization: Bearer {{employee_role_token}}
pm.test("Nhân viên thường không được xoá user", function () {
pm.response.to.have.status(403);
});
Bước 6 — Test các edge case của token. Thêm request với token rỗng, token hết hạn, và token đúng định dạng nhưng sai chữ ký. Tất cả phải trả 401:
pm.test("Không token thì 401", function () {
pm.response.to.have.status(401);
});
Bước 7 — Gom thành một folder "Authorization & Multi-tenant" trong collection và chạy bằng Collection Runner hoặc Newman (Bài 13). Bộ test này nên chạy như một cổng kiểm soát: nếu bất kỳ assert nào fail, coi như build không được phép lên production.
Lỗi thường gặp & mẹo
Chỉ test happy path. Đây là sai lầm số một. Nhiều tester dừng lại khi "user xem đúng data của mình". Nhưng authorization chỉ được chứng minh khi bạn thử những gì không được phép và thấy nó bị chặn. Test authorization mà không có cross-tenant case thì coi như chưa test.
Nhầm 401 với 403. Hai mã này khác nhau về ngữ nghĩa: 401 Unauthorized nghĩa là "tôi không biết bạn là ai" (thiếu/sai token — vấn đề authentication). 403 Forbidden nghĩa là "tôi biết bạn là ai, nhưng bạn không có quyền" (vấn đề authorization). Test của bạn phải assert đúng mã cho đúng ngữ cảnh, đừng viết oneOf([401,403]) một cách lười biếng cho mọi trường hợp — vì như vậy bạn sẽ không phát hiện được khi server trả nhầm mã.
Chỉ assert status code mà quên assert body. Một số hệ thống lỗi trả về status 403 nhưng body vẫn chứa data thật (do middleware set status sau khi đã serialize data). Luôn assert thêm rằng body không chứa trường nhạy cảm của tenant khác.
Quên test tenant_id truyền từ client. Như tình huống SmartPOS, nếu API nhận tenant_id từ query/body, hãy luôn thử gửi tenant_id của người khác kèm token của mình. Mẹo: test cả trường hợp gửi hai tenant_id mâu thuẫn (một trong token, một trong body) — server phải luôn ưu tiên token.
Token bị lẫn giữa các môi trường. Đừng để token staging lọt vào biến production trong Environment. Đặt tên biến rõ ràng theo môi trường, và dùng Vault (Bài 21) cho secret để tránh commit token vào git.
Mẹo tự động hoá ma trận: thay vì viết tay từng request, dùng data-driven testing với một file CSV có các cột actor_token, target_id, expected_status. Một request duy nhất lặp qua toàn bộ ma trận — vừa gọn, vừa dễ mở rộng khi thêm actor mới.
Mẹo đọc JWT nhanh: trong Pre-request Script (Bài 7), bạn có thể decode phần payload của JWT để lấy tenant_id mà không cần hỏi backend: JSON.parse(atob(token.split('.')[1])). Rất hữu ích để dựng test khẳng định "token này thuộc tenant nào" trước khi tấn công chéo.
Bài tập thực hành
- Dựng test matrix của riêng bạn. Chọn một API multi-tenant thật hoặc giả định (ví dụ một API quản lý đơn hàng). Vẽ bảng ma trận với ít nhất 3 actor (admin tenant A, nhân viên tenant A, user tenant B) và 3 hành động (GET, POST, DELETE). Điền kết quả mong đợi vào từng ô, đánh dấu rõ ô nào phải là 403/404.
- Viết request cross-tenant. Với token của tenant A, gọi một tài nguyên của tenant B. Viết Tests script assert status là 403 hoặc 404 VÀ body không chứa dữ liệu thật của tenant B.
- Tự động hoá IDOR bằng CSV. Tạo file
victims.csvvới cộttarget_idgồm 5 ID thuộc tenant khác. Chạy request qua Collection Runner với token của bạn và assert cả 5 đều bị chặn. Cố ý thêm một dòng là ID hợp lệ của chính bạn để xác nhận test phân biệt được đúng/sai.
- Test privilege escalation. Dùng token role thấp gọi một endpoint admin-only, assert 403. Sau đó dùng token admin gọi cùng endpoint, assert 200 — chứng minh test của bạn không "chặn nhầm" người có quyền.
- Nâng cao: viết Pre-request Script decode JWT để tự động lấy
tenant_idcủa token đang dùng, in ra console, rồi build một biếnotherTenantIdđể tấn công chéo mà không cần hard-code.
Tóm tắt
Test multi-tenant và authorization edge case là kỹ năng đóng vai "kẻ tấn công có kỷ luật": bạn không chỉ xác nhận người dùng làm được việc của mình, mà chủ động chứng minh họ không làm được việc của người khác. Ba trụ cột cần nhớ:
- Test cả hai trục. Chiều ngang (cross-tenant — IDOR) và chiều dọc (cross-role — privilege escalation) là hai lỗi độc lập, phải test riêng.
- Dùng test matrix làm xương sống. Liệt kê actor × tài nguyên × hành động, điền kết quả mong đợi, rồi tự động hoá bằng data-driven CSV để không bỏ sót ô nào.
- Assert cả status lẫn body. 401 cho thiếu danh tính, 403/404 cho thiếu quyền, và tuyệt đối không được có 200 kèm dữ liệu tenant khác. Không bao giờ tin
tenant_iddo client gửi.