Security và Authentication Testing mà QA hoàn toàn có thể tự làm

Dịch từ blog của tác giả Dịch từ blog của tác giả juliapottinger
Tôi từng test một nền tảng bán lẻ chạy hai storefront trên cùng một backend. Một phần công việc của tôi là chứng minh rằng người mua hàng ở brand này không bao giờ chạm được tới đơn hàng thuộc về brand kia. Nên tôi đăng nhập bằng một user, bắt lấy request lấy đơn hàng của chính mình, sửa đúng một con số trong đó để trỏ sang đơn của người khác, rồi gửi lại — vẫn với token đăng nhập của mình.
Chỉ một thay đổi nhỏ đó thôi đã là toàn bộ bài test, và đó là bài test mà automated scanner không làm thay bạn được. Request trông hoàn toàn hợp lệ: user thật, session thật, endpoint bình thường. Thứ duy nhất sai là đơn hàng đó thuộc về ai — và scanner không hề biết ai được phép sở hữu cái gì. Tôi thì biết, vì tôi nắm data model. Nên tôi mới là người bắt được lỗi này.
Bài test đó đã thay đổi cách tôi nhìn về security. Trong một thời gian dài, với tôi nó cũng nghe như “việc của người khác”: của chuyên gia với màn hình terminal đen và một đống chứng chỉ, của kỳ penetration test hằng năm mà kết quả là một file PDF chẳng ai đọc cho đến mùa audit. Thế là chúng ta chỉ kiểm tra form login có chạy không, xác nhận ô password có che ký tự không, rồi đi tiếp — mặc định rằng phần đáng sợ đã có ai đó lo rồi.
Thường thì không có ai lo cả. Những bug gây thiệt hại nặng nhất không hề cao siêu:
- Một user đọc được đơn hàng của user khác chỉ bằng cách đổi số trong URL.
- Một session vẫn còn hiệu lực sau khi đã logout.
- Luồng reset password làm lộ token.
- Trang lỗi in nguyên câu truy vấn database ra cho kẻ tấn công đọc.
Không thứ nào cần đến chuyên gia. Nó chỉ cần một người biết gửi request, đọc response, và đặt đúng câu hỏi mà QA vẫn đặt cho mọi thứ khác: chỗ này có thể vỡ ở đâu?
Nhiều năm test fintech và e-commerce dạy tôi phải hỏi câu đó với bất cứ thứ gì dính đến tiền hoặc danh tính. Và khi tự làm một game mà số dư lẫn tiến trình của người chơi nằm trên server, tôi lại thấm điều đó thêm lần nữa. Khi tiền thật và dữ liệu thật nằm sau một endpoint, câu hỏi “cái này đủ an toàn để ship chưa” không còn là việc của người khác.
Thứ bạn có thể tự sở hữu là một security baseline lặp lại được, chạy trên chính web app và API bạn đang test, bằng chính những công cụ bạn đã mở sẵn.
Bắt đầu từ nơi rủi ro thật sự nằm
Hãy neo vào danh sách chuẩn để khỏi phải đoán xem cái gì mới quan trọng. OWASP Top 10 xếp hạng những rủi ro bảo mật web application nghiêm trọng nhất.
Ở phiên bản 2025 — lần cập nhật đầu tiên kể từ 2021 — hai vị trí đầu là broken access control và security misconfiguration, cùng một cái tên mới ở vị trí thứ ba: software supply chain failures. Authentication failures vẫn còn trong danh sách, tụt xuống A07.
Broken access control giữ vị trí số 1 nhiều năm liền, và đó không phải ngẫu nhiên. Nó là loại bug dễ ship nhất và dễ bỏ sót nhất, bởi vì happy path trông hoàn hảo.
Nếu sản phẩm của bạn có backend, danh sách còn quan trọng hơn nữa là OWASP API Security Top 10 — vì phần web hay mobile front end thực chất chỉ là lớp vỏ mỏng bọc quanh API, và mọi quyết định về quyền hạn đều nằm ở đó.
Rủi ro số 1 của nó là API1:2023 — Broken Object Level Authorization (BOLA), tên gọi phía API của thứ mà bạn có thể quen với cái tên Insecure Direct Object Reference (IDOR). Cùng một bug với broken access control bên web, chỉ là nhìn từ phía API — và là thứ đáng học nhất đối với một QA.
| OWASP Top 10 (web, 2025) | OWASP API Top 10 (2023) |
|---|---|
| #1 Broken access control (giữ #1 nhiều năm) | #1 BOLA (object-level) — tên gọi phía API của IDOR |
| #2 Security misconfiguration | |
| #3 Software supply chain |
Cùng một bug, nhìn từ hai phía. Bài test id-swap bên dưới chứng minh được nó ở cả hai. Nếu happy path trông hoàn hảo, hãy soi chỗ này trước.
Những câu hỏi tôi đặt trước khi sign off phần auth
Security testing là risk thinking nhưng sắc hơn một chút, nên nó khởi đầu giống hệt phần còn lại trong công việc test của tôi. Hãy chạy feature qua các câu hỏi này trước khi mở bất kỳ tool nào:
- User A có với tới được dữ liệu của user B chỉ bằng cách đổi một id trong request không?
- Chuyện gì xảy ra khi không có token, token hết hạn, và token của sai role?
- Những hành động nào được kiểm tra quyền ở server, chứ không chỉ là ẩn một cái nút trên màn hình?
- Có dữ liệu nhạy cảm nào đang nằm trong URL, log, hay response ở chỗ lẽ ra không được nằm không?
- Và câu hỏi định phạm vi cho tất cả những câu trên: điều tệ nhất mà một user có động cơ xấu làm được ở đây là gì?
Tôi sẽ không đi hết cả top ten. Tôi sẽ đưa bạn một baseline chạy được thật, theo đúng thứ tự tôi test:
- Access control (IDOR / BOLA, và vertical privilege escalation)
- Authentication và session (login, lockout, cookie, token, logout)
- Transport và header (chỉ HTTPS, các security response header)
- Secret và rò rỉ thông tin (key nằm trong bundle, error message nói quá nhiều)
- Input (probe cho SQL injection và cross-site scripting)
Mỗi mục là một câu hỏi bạn trả lời được bằng một proxy, Postman, DevTools của trình duyệt, và một chút kiên nhẫn.
Access control: bài test mà thường chỉ QA mới thật sự chạy
Bài test hai brand ở đầu bài là thứ nên làm đầu tiên, và cần nói rõ vì sao nó rơi vào tay QA. Automated scanner rất kém với broken access control, vì request trông hoàn toàn hợp lệ: đúng user, session thật, endpoint bình thường. Thứ duy nhất sai là object ở đầu bên kia thuộc về người khác — mà scanner thì không biết ai được sở hữu cái gì.
Bạn thì biết. Bạn nắm data model, bạn biết user A không bao giờ được thấy đơn của user B, nên bạn là người chứng minh được điều đó.
Cách làm, đơn giản đến mức học thuộc được
1. Login bằng User A, bắt lấy một request của chính mình
GET /api/orders/1001 Authorization: Bearer A
2. Thay bằng id của User B, giữ nguyên token của User A
GET /api/orders/1002 Authorization: Bearer A
3. Đọc response
| Kết quả | Ý nghĩa |
|---|---|
403 Forbidden hoặc 404 Not Found | ✅ An toàn |
200 OK trả về dữ liệu của User B | 🐞 Bug |
Cái 200 đó chính là bug — và là một trong những finding nghiêm trọng phổ biến nhất hiện nay.
Ba điều biến bài test này từ “chọc thử một lần” thành coverage thật
- Lặp lại trên tất cả các verb. Một
GETcó thể đã khóa chặt trong khiPUThayDELETEthì mở toang, vì ai đó thêm check cho read mà quên phần còn lại. Hãy thử đọc, sửa, và xóa object của người khác. Một user không xem được đơn của user B nhưng xóa được thì còn tệ hơn. - Test luôn cả trường hợp không token và token sai. Gửi cùng request đó mà không có header
Authorization, rồi gửi với token hết hạn. Cả hai đều phải bị từ chối. Chuyện endpoint kiểm tra quyền sở hữu với user đã đăng nhập nhưng lại vui vẻ trả lời request unauthenticated xảy ra nhiều đến bất ngờ. - Thử đi lên, không chỉ đi ngang. Đó là vertical privilege escalation, thứ OWASP gọi là Broken Function Level Authorization (BFLA). Lấy token của một user thường và gọi một endpoint chỉ dành cho admin, ví dụ
DELETE /api/admin/users/55hayGET /api/admin/reports. Kết quả phải là403. Ẩn nút admin trên UI không phải là access control — endpoint vẫn nằm đó, và front end không phải là thứ chặn được ai cả.
Trong thực tế, bài kiểm tra trông như sau:
# Object của chính mình: được phép
GET /v1/orders/8412 Authorization: Bearer <your-token>
→ 200 OK (đơn hàng của bạn)
# Cùng request, token của bạn, id của người khác: phải bị từ chối
GET /v1/orders/9001 Authorization: Bearer <your-token>
→ 403 Forbidden { "type": "about:blank", "title": "Forbidden", "status": 403 }
Điều kiện pass là cái 403 thứ hai. Nếu request đã đổi id mà trả về 200, bạn vừa tìm ra một lỗ hổng authorization thật — đúng thứ mà bài test này sinh ra để bắt.
Công cụ
Burp Suite Community Edition là lựa chọn của tôi cho việc này. Duyệt web với tư cách user A khi bật proxy của Burp, tìm request trong history, gửi sang Repeater, đổi id, gửi lại — và theo dõi status code cùng body thay đổi ngay trước mắt.
Community Edition có Repeater và proxy, đủ dùng cho việc này. Nó thiếu automated active scanner (tính năng trả phí), nhưng với access control thì scanner cũng chẳng giúp được gì.
OWASP ZAP làm được đúng việc đó, hoàn toàn miễn phí và mã nguồn mở, kèm passive scanner chỉ ra các vấn đề khác trong lúc bạn làm việc.
Và một khi đã chứng minh được bằng tay, hãy tự động hóa nó — đây là lúc Postman phát huy giá trị.
Tự động hóa bài test access control bằng Postman
Chứng minh thủ công chỉ là khởi đầu. Thứ bảo vệ bạn về lâu dài là một request được lưu lại kèm assertion, để nó chạy trong mọi lượt regression thay vì sống trong trí nhớ của bạn. Trong Postman, phần này nằm ở tab Tests (tab Scripts ở bản mới hơn), chạy sau khi response trả về.
Setup: một request gọi tới object của user B, nhưng authenticate bằng token của user A. Để token của user A trong một collection variable hoặc environment variable cho dễ thay. Assertion khi đó chỉ là một dòng dễ đọc.
// Tab Tests. Request: GET {{baseUrl}}/api/orders/1002
// Auth: Bearer {{userA_token}} (token thuộc về User A, không phải User B)
pm.test("User A cannot read User B's order", function () {
pm.expect(pm.response.code).to.be.oneOf([403, 404]);
});
// Bắt rõ trường hợp rò rỉ âm thầm: 200 ở đây chính là finding.
pm.test("Response does not expose another user's data", function () {
if (pm.response.code === 200) {
const body = pm.response.json();
pm.expect.fail(
`Expected 403/404 but got 200 returning order owner ${body.userId}`
);
}
});
Sau đó nhân bản request cho các trường hợp negative auth, vì chúng là một phần của cùng một control:
// Cùng endpoint, nhưng KHÔNG gửi header Authorization, rồi gửi token ĐÃ HẾT HẠN.
pm.test("Unauthenticated request is rejected", function () {
pm.expect(pm.response.code).to.be.oneOf([401, 403]);
});
Gom chúng vào một folder “negative auth” trong collection, trỏ Newman hoặc job CI của bạn vào đó — và broken access control trở thành thứ pipeline tự bắt, thay vì thứ bạn hy vọng có ai đó nhớ kiểm tra bằng tay.
Authentication và session: test cả vòng đời, không chỉ màn hình login
Test cho login chạy được thì dễ. Bug nằm ở mọi thứ xung quanh nó. OWASP Web Security Testing Guide (WSTG) là cẩm nang thực tế ở đây, dễ đọc kể cả khi bạn chưa từng mở một tài liệu security nào. Đây là những check tôi chạy.
Credential không bao giờ đi qua HTTP thuần
Theo dõi request login trong DevTools hoặc proxy. Nó phải là HTTPS. Một form login POST qua HTTP — kể cả loại “redirect sang HTTPS sau đó” — thì password đã lộ ngay trên đường đi rồi.
Brute force phải có trần
Dội login bằng password sai liên tục. Bắt buộc phải có account lockout, rate limiting, hoặc cả hai — nếu không thì ai đó có thể đoán mãi mãi. WSTG nêu rõ điều này trong nhóm authentication test, và việc hoàn toàn không có throttling là một finding thật sự.
Password policy theo hướng dẫn hiện hành, không theo thói quen cũ
Chỗ này hay làm nhiều người bất ngờ, vì các quy tắc cũ giờ đã sai. NIST SP 800-63B đặt ra chuẩn hiện đại, và việc của tester là kiểm tra sản phẩm có khớp không:
- Tối thiểu ít nhất 15 ký tự, và cho phép tối thiểu 64 ký tự (passphrase dài phải được chấp nhận, không bị cắt bớt)
- Không ép composition rule (không bắt buộc phải có đủ hoa, thường, số, ký tự đặc biệt)
- Không ép đổi password định kỳ (bắt đổi mỗi 90 ngày giờ bị khuyến cáo không nên làm)
- Password mới phải được đối chiếu với blocklist password đã bị lộ, để password đã biết là compromised bị từ chối
Nếu sản phẩm vẫn đòi một chữ hoa, một ký tự đặc biệt và đổi mỗi quý, đó không phải an toàn thêm — đó là một finding trái với chuẩn hiện hành.
Session được cấu hình đúng
Đây là nơi phần lớn bug về session ẩn mình, và DevTools cho bạn thấy tất cả. Mở tab Application/Storage và kiểm tra session cookie:
HttpOnlyđược bật, để JavaScript không đọc được cookie, và một bug XSS không thể lấy trộm nóSecuređược bật, để cookie chỉ đi qua HTTPSSameSiteđược set (LaxhoặcStrict), làm giảm nguy cơ CSRF- Logout thật sự vô hiệu hóa session ở phía server, chứ không chỉ xóa trong trình duyệt. Bắt lấy một session token hợp lệ, logout, rồi replay lại request với token cũ. Nó phải bị từ chối. Nếu token cũ vẫn dùng được thì logout chỉ là lời nói dối.
- Session timeout sau một khoảng idle hợp lý
- Không thể fix session: một session id được cấp cho bạn trước khi login không được phép vẫn còn hiệu lực — và giờ có thêm quyền — sau khi login. Đó chính là session fixation.
Nếu hệ thống dùng token, hãy test token
Rất nhiều backend phát hành JSON Web Token (JWT, RFC 7519). Bạn không cần là chuyên gia mật mã. Dán nó vào DevTools và thử ba việc mà server bắt buộc phải từ chối:
- Đặt
algtrong header thànhnone, bỏ phần signature, rồi gửi đi. Server chấp nhận một token không ký là lỗi cực kỳ nghiêm trọng, vì khi đó ai cũng tự đúc được token cho bất kỳ ai. - Đổi một ký tự trong payload (đổi user id của bạn, hoặc
"admin": falsethànhtrue) rồi gửi. Signature không còn khớp, nên nó phải bị từ chối. - Gửi một token đã hết hạn, quá
expclaim. Nó phải bị từ chối, không được lặng lẽ chấp nhận.
Toàn bộ baseline authentication này không cần gì ngoài trình duyệt và một proxy.
Transport và header: test rất rẻ, bỏ sót thì rất quê
Đây là phần nhanh nhất của baseline, và cũng là phần dễ tìm ra vấn đề nhất trên app thật. Hai lớp: kết nối có được mã hóa không, và trình duyệt có được chỉ dẫn cư xử an toàn không.
Về transport: xác nhận toàn bộ site chạy HTTPS, các request http:// được redirect sang https://, và header Strict-Transport-Security có mặt để trình duyệt từ chối hạ cấp kết nối.
Khi đang mở DevTools, để ý luôn mixed content — một trang HTTPS nhưng lại tải script hoặc ảnh qua HTTP thuần; console sẽ cảnh báo bạn.
Về response header, OWASP Secure Headers Project là tài liệu tham chiếu. Đọc thẳng trên tab Network, hoặc bằng một lệnh curl -I https://yoursite.
| Header | Tác dụng | Giá trị ví dụ |
|---|---|---|
Strict-Transport-Security | Ép HTTPS; trình duyệt từ chối hạ cấp | max-age=63072000; includeSubDomains |
Content-Security-Policy | Giới hạn nơi script, style, frame được tải về; phòng thủ mạnh trước XSS | default-src 'self' |
X-Content-Type-Options | Chặn trình duyệt tự đoán kiểu file (MIME sniffing) | nosniff |
X-Frame-Options | Chặn trang bị site khác nhúng vào frame (clickjacking) | deny |
Referrer-Policy | Kiểm soát mức độ URL bị rò sang site khác | strict-origin-when-cross-origin |
Permissions-Policy | Tắt các tính năng trình duyệt mà app không dùng | geolocation=(), camera=() |
Thiếu một header không phải lúc nào cũng là thảm họa. Nhưng một luồng login hoặc thanh toán mà không có Strict-Transport-Security và Content-Security-Policy thì lần nào cũng đáng raise. Đây chỉ là vài dòng config server, nên chi phí fix rất rẻ một khi đã được chỉ ra.
Tool nào cho check nào
Bạn sẽ không dùng mọi tool cho mọi check. Đây là cách bộ công cụ QA thực tế ánh xạ vào baseline:
| Check | OWASP ZAP | Burp Community | Postman | DevTools |
|---|---|---|---|---|
| Đổi id thủ công cho IDOR / BOLA | ✓ | ✓ | ~ | ✗ |
| Tự động hóa regression cho access control | ~ | ✗ | ✓ | ✗ |
| Kiểm tra cookie flag và token | ~ | ~ | ✗ | ✓ |
| Replay request với token đã bị sửa | ✓ | ✓ | ✓ | ✗ |
| Kiểm tra security response header | ✓ | ~ | ~ | ✓ |
| Passive scan tìm lỗi dễ thấy | ✓ | ✗ | ✗ | ✗ |
✓ tool tự nhiên cho việc này ~ làm được nhưng không phải sở trường ✗ không phải tool phù hợp
Nếu chỉ cài đúng một thứ, hãy chọn một intercepting proxy (ZAP hoặc Burp Community) — vì nhìn thấy và replay được traffic thật là kỹ năng nền cho tất cả những phần còn lại. curl lo được phần header và token từ script, còn Postman biến các check access control thủ công thành một bộ test chạy mãi về sau.
Secret và rò rỉ: hãy khiến app “nói hớ”
Hai kiểu lỗi liên quan, đều phổ biến và đều dễ test: app đang mang theo một secret mà nó không nên phơi ra, hoặc lỗi của nó tiết lộ nội tạng bên trong.
Secret
Front end không phải nơi an toàn để giữ bất cứ thứ gì, vì mọi thứ được gửi xuống trình duyệt hay thiết bị đều đọc được. Mở JavaScript bundle trong tab Network hoặc Sources và tìm các từ key, secret, token, password.
Một API key private, một cloud credential, hay một signing secret nằm trong client code là một finding thật, vì ai cũng đọc được. Làm y hệt với API response và error body.
Trong repository, xác nhận file .env và các key material không bị commit, và có scan tự động chặn secret lọt vào sau này. git-secrets quét commit tìm pattern credential trước khi chúng được ghi vào; TruffleHog đào xuyên toàn bộ lịch sử repo để tìm key bị lộ. Cả hai đều dễ gắn vào CI.
Rò rỉ qua error
Hãy cố tình làm hỏng. Gửi JSON sai định dạng, gửi string vào chỗ mong đợi number, gửi payload cực lớn, gửi id không tồn tại. Rồi đọc kỹ thứ trả về.
Một app an toàn trả về error chung chung, có kiểm soát. Một app rò rỉ sẽ ném cho bạn nguyên stack trace, một raw SQL error, một đường dẫn file nội bộ, hoặc chính xác framework và version nó đang chạy — tất cả đều là bản đồ miễn phí cho kẻ tấn công. Cách sửa là server log chi tiết ở chỗ riêng tư và trả về message sạch sẽ, và việc verify điều đó rõ ràng là việc của QA.
Input: những probe injection nên luôn có sẵn trong túi
Injection testing đầy đủ là một lĩnh vực sâu, nhưng có hai probe nhanh nên nằm trong baseline của mọi tester, vì hậu quả rất nặng còn cách kiểm tra thì rất đơn giản.
SQL injection: thả một dấu nháy đơn ' vào field mà bạn nghi là chạm tới database — ô search, ô login, một bộ filter. Nếu trang ném ra database error, nghĩa là input đang đi thẳng vào câu truy vấn một cách không an toàn — đây là finding đáng escalate. Một probe an toàn hơn là đoạn “luôn đúng” kinh điển như ' OR '1'='1 trong ô login. Nó phải không đăng nhập được.
Cross-site scripting (XSS): gửi <script>alert(1)</script> hoặc <img src=x onerror=alert(1)> vào field mà giá trị của nó được hiển thị lại trên trang — tên, comment, bio hồ sơ. Nếu một hộp thoại bật lên, app đang render input của bạn thành code sống thay vì text thuần.
Kiểm tra cả ở chỗ bạn nhập lẫn mọi nơi khác giá trị đó xuất hiện lại, vì stored XSS thường nổ ở một màn hình khác với màn hình bạn submit.
Hai probe này không biến bạn thành penetration tester, và cũng không nhằm mục đích đó. Chúng bắt sớm những ca hiển nhiên và nghiêm trọng, đồng thời cho bạn biết chỗ nào cần nhờ soi sâu hơn.
AI giúp được ở đâu, và không giúp được ở đâu
Một coding agent như Claude Code thật sự hữu ích ở đây, miễn là bạn thành thật về việc nó giúp phần nào. Nó rất nhanh trong việc rà một API response khổng lồ để tìm một field lẽ ra không bao giờ được trả về — một internal flag, email của user khác, một password hash nằm trong payload — đúng loại việc mà soi bằng mắt qua hàng trăm dòng thì cực kỳ mệt. Nó cũng giỏi sinh ra các biến thể payload cho injection và fuzzing, nhanh hơn bạn gõ tay nhiều.
Thứ nó không sở hữu là phán đoán. Cái 200 đó có thật sự quan trọng không, field trả về kia là rò rỉ thật hay vô hại, một finding có đáng chặn release hay không — đó là risk thinking, và nó vẫn thuộc về bạn.
AI mở rộng cái lưới; bạn quyết định con nào trong lưới là cá thật.
Đừng bao giờ tin client — trên mobile thì đó là toàn bộ cuộc chơi
Mọi thứ ở trên đều trở nên gắt hơn trên mobile, môi trường thù địch nhất. App chạy trên một thiết bị bạn không kiểm soát, và một user quyết tâm có thể inspect nó, sửa nó, và nói dối server bằng nó. Ai từng test app mobile với tài khoản thật và giao dịch thật đều buộc phải giả định đúng như vậy.
OWASP Mobile Application Security Verification Standard (MASVS) nói thẳng: hãy coi client như đã bị chiếm quyền, và mọi control bạn đặt ở đó chỉ là defence in depth, không bao giờ là thứ thật sự bảo vệ bạn.
Trên thực tế, server là nguồn sự thật cho ba thứ quan trọng: danh tính, tiền, và quyền lợi (entitlement). Một điểm số do client báo, một số dư client khai, một câu “vâng user này đã trả tiền” do client khẳng định — không thứ nào tự nó đáng tin.
- Điểm leaderboard phải được validate ở server, nếu không sẽ có người post một tỷ điểm.
- Số dư coin phải nằm trên server, nếu không sẽ có người sửa nó ở local.
- Giao dịch mua phải được verify với Apple App Store Server API hoặc Google Play Developer API ở backend của bạn, không phải tin lời client.
Góc nhìn security thêm đúng một câu hỏi vào chuỗi này: nếu tôi nói dối server từ một client đã bị hack, nó có tin không? Hãy chứng minh câu trả lời trên server, không phải trên màn hình.
Checklist security baseline cho QA (copy-paste được)
Đây là toàn bộ nội dung dưới dạng checklist, nhóm theo đúng cách tôi chạy, thật sự làm được, và không mục nào cần chuyên gia security. Dán vào test plan, chỉnh endpoint cho khớp app của bạn, và đưa nó vào các lượt regression và pre-release.
Access control
- [ ] Đăng nhập bằng user A, request object của user B trả về
403hoặc404, không bao giờ200kèm dữ liệu của B - [ ] id-swap bị chặn trên cả
GET,PUT, vàDELETE, không chỉ mỗi read - [ ] Cùng request đó không kèm token thì bị từ chối (
401/403) - [ ] Cùng request đó với token hết hạn thì bị từ chối
- [ ] Token của user thường không gọi được endpoint admin (vertical escalation trả về
403) - [ ] Các check access control được lưu thành automated assertion, không phải làm tay một lần rồi thôi
Authentication và session
- [ ] Credential chỉ đi qua HTTPS
- [ ] Login sai nhiều lần liên tiếp chạm account lockout hoặc rate limiting
- [ ] Quy tắc password khớp hướng dẫn hiện hành: độ dài tối thiểu lớn, không ép composition, không ép đổi định kỳ, từ chối password đã bị lộ
- [ ] Session cookie có
HttpOnly,Secure, vàSameSite - [ ] Logout vô hiệu hóa session ở server (replay token cũ sau logout phải fail)
- [ ] Session timeout sau khi idle, và session fixation không thể xảy ra
- [ ] Các check JWT pass: từ chối
alg: none, từ chối payload bị sửa, từ chối token hết hạn
Transport và header
- [ ] Toàn bộ site là HTTPS và
http://redirect sanghttps:// - [ ] Có
Strict-Transport-Security - [ ] Đã set
Content-Security-Policy,X-Content-Type-Options: nosniff,X-Frame-Options,Referrer-Policy, vàPermissions-Policy - [ ] Không có mixed content (trang HTTPS tải tài nguyên không an toàn)
Secret và rò rỉ
- [ ] Client bundle, API response, và error body không chứa key, token, hay password nào
- [ ] File
.envvà key material không bị commit; secret scanning chạy trong CI - [ ] Lỗi cố tình gây ra trả về message sạch, không có stack trace, SQL error, đường dẫn nội bộ, hay version framework
Input
- [ ] Một dấu
'trong field chạm database không tạo ra raw database error - [ ]
' OR '1'='1trong ô login không đăng nhập được - [ ]
<script>alert(1)</script>được render thành text, không thực thi, ở mọi nơi giá trị đó xuất hiện
Gắn chủ sở hữu và điều kiện hoàn thành
Để việc này không biến thành màn “một mình gánh team”, hãy gắn owner và finish line cho những phần quyết định nó có thật sự chạy hay không:
- Owner: QA. Chạy bài id-swap cho access control trên tất cả các verb và các case negative auth. Hoàn thành khi: user A request object của user B trả về
403hoặc404trênGET,PUT,DELETE, và request không token / token hết hạn đều bị từ chối. - Owner: Dev. Đưa các assertion Postman/Newman đã lưu vào CI. Hoàn thành khi: các check access control và negative auth chạy trong mọi lượt pipeline, và một
200rò rỉ làm fail build. - Owner: EM. Quyết định cái gì chặn release. Hoàn thành khi: đã ghi rõ thành văn bản rằng finding về broken access control hoặc token không ký sẽ chặn ship, còn thiếu header thì được log lại chứ không gate.
Bắt đầu từ đâu
Bạn không cần làm tất cả ngay ngày đầu tiên, và cũng không nên cố. Hãy chọn bài test access control trước, vì nó tìm ra những bug nghiêm trọng nhất và là bài mà chỉ QA mới chạy đều đặn.
Cài một proxy, đăng nhập bằng hai user, đổi một id, và xem thứ trả về là gì. Chỉ riêng bài tập đó — làm trên chính sản phẩm thật của bạn ngay tuần này — sẽ dạy bạn về bảo mật của ứng dụng nhiều hơn bất kỳ checklist nào, và nhiều khả năng sẽ tìm ra được thứ gì đó.
Từ đó, gộp dần từng nhóm còn lại vào, tự động hóa các check access control và negative auth để chúng chạy trong mọi lượt, và để mấy check header và token nhàm chán đi kèm trong curl.
Bạn không đang cố thay thế penetration test. Bạn đang bắt trước những bug mà nếu không thì chúng sẽ đến tay pentest — hoặc đến tay người dùng. Hãy hướng bản năng “chỗ này có thể vỡ ở đâu” vào danh tính, tiền, và những object mà user sở hữu, chứng minh điều đó trên server, và bạn sẽ ship với bằng chứng thay vì bắt chéo ngón tay cầu may.
