OWASP API Security Top 10 (2023) trên shop-app
Tiếp đi: OWASP API Top 10. Pentest sắp tới sẽ kiểm tra shop-app theo danh sách này. Mỗi mục trông thế nào trên API của mình?
1. Bài toán
shop-app có endpoint cho buyer, seller (partner) và admin. Pentester đăng ký một account buyer bình thường và thử mọi cách lấy dữ liệu hoặc làm việc vượt quyền.
2. Vấn đề
Lỗ hổng API phần lớn không phải SQL injection. Đó là lỗi logic phân quyền: code chạy đúng, test pass, nhưng thiếu một câu "người này có được làm việc này với object này không?".
3. Giải pháp: đi qua 10 mục
| # | Mục | Ví dụ trên shop-app | Chống |
|---|---|---|---|
| API1 | Broken Object Level Authorization (BOLA) | GET /orders/1002 đổi ID xem order người khác | Kiểm tra owner ở mọi truy cập object |
| API2 | Broken Authentication | Login không giới hạn thử, JWT alg:none, token không hết hạn | Rate limit login, MFA, validate JWT đúng (Keycloak) |
| API3 | Broken Object Property Level Authorization (BOPLA) | Gửi "role":"ADMIN" khi sửa profile (mass assignment); response lộ passwordHash | DTO riêng cho input/output, allowlist field |
| API4 | Unrestricted Resource Consumption | ?limit=1000000, upload 2GB, gửi 10k OTP SMS | Giới hạn page size, body size, timeout, rate limit, quota |
| API5 | Broken Function Level Authorization (BFLA) | Buyer gọi DELETE /admin/products/7 | Deny-by-default theo role ở mọi endpoint |
| API6 | Unrestricted Access to Sensitive Business Flows | Bot mua sạch hàng flash sale, spam voucher | Giới hạn theo user/thiết bị, captcha ở flow, phát hiện bot |
| API7 | Server Side Request Forgery (SSRF) | "Import ảnh từ URL": http://169.254.169.254/... | Allowlist host, chặn IP nội bộ, không follow redirect |
| API8 | Security Misconfiguration | Actuator mở public, CORS *, stack trace trong lỗi | Hardening, scan config, header bảo mật |
| API9 | Improper Inventory Management | /v1 cũ quên tắt, staging-api dùng dữ liệu thật | Danh mục API + version, gateway, sunset |
| API10 | Unsafe Consumption of APIs | Tin response của payment/shipping partner, không validate | Validate, timeout, TLS, coi API ngoài như input user |
4. Code từng bước
Bước 1: chống BOLA, đưa owner vào query:
// Sai: tìm theo id rồi trả luôn
Order o = repo.findById(id).orElseThrow();
// Đúng: id + owner, không thấy thì 404
public OrderDto get(String orderId, Jwt jwt) {
return repo.findByIdAndCustomerId(orderId, jwt.getSubject())
.map(mapper::toDto)
.orElseThrow(() -> new NotFoundException("ORDER_NOT_FOUND"));
}
Bước 2: chống BOPLA / mass assignment, DTO allowlist:
public record UpdateProfileRequest(
@Size(max = 100) String displayName,
@Pattern(regexp = "^\\+?[0-9]{9,15}$") String phone) {} // không có role, không có balance
public record ProfileResponse(String id, String displayName, String phone) {} // không có passwordHash
Bước 3: chống BFLA và API4
http.authorizeHttpRequests(a -> a
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/sellers/*/products").hasRole("SELLER")
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()); // deny-by-default
// API4
int limit = Math.min(requestedLimit, 100);
// application.yml: spring.servlet.multipart.max-file-size: 5MB
Bước 4: test như pentester
curl -i https://api.shop.example/v1/orders/ord_of_user_B -H "Authorization: Bearer $TOKEN_A" # mong đợi 404
curl -i -X PATCH .../me -H "Authorization: Bearer $TOKEN_A" -d '{"role":"ADMIN"}' # role không đổi
curl -i -X DELETE .../admin/products/7 -H "Authorization: Bearer $TOKEN_A" # 403
curl -i ".../products?limit=1000000" # trả tối đa 100
Tốt nhất viết thành integration test tự động: mỗi endpoint có ít nhất một test "user khác truy cập".
5. Cách hoạt động: các lớp kiểm tra
Request
│
├─ Gateway / WAF ........ rate limit, body size, chặn path admin từ Internet (API4, API8)
├─ AuthN ................ token hợp lệ, chưa hết hạn, đúng issuer/audience (API2)
├─ Function authz ....... role có được gọi endpoint này? (API5)
├─ Input DTO ............ chỉ field cho phép, validate (API3)
├─ Object authz ......... object này thuộc user? (API1)
├─ Business rules ....... giới hạn flow (mỗi user 2 sp flash sale) (API6)
├─ Outbound calls ....... allowlist URL, validate response (API7, API10)
└─ Output DTO ........... chỉ field được xem (API3)
6. So sánh: BOLA vs BFLA vs BOPLA
| Câu hỏi bị bỏ quên | Ví dụ | |
|---|---|---|
| BOLA | Object này có phải của bạn? | /orders/1002 |
| BFLA | Bạn có được gọi chức năng này? | /admin/... |
| BOPLA | Bạn có được đọc/ghi field này? | role, passwordHash |
7. Khi nào nên dùng / không nên dùng
- Dùng danh sách này làm checklist khi review PR thêm endpoint và khi threat modeling.
- ID khó đoán (UUID,
ord_8f3k2) giúp giảm dò, nhưng không thay kiểm tra owner. - Gateway không biết ownership nghiệp vụ: BOLA phải chặn trong service.
- Không trả
EntityJPA thẳng ra JSON.
Seller có tính năng "nhập URL ảnh, server tải về làm ảnh sản phẩm". Mình chặn localhost và 127.0.0.1 trong URL rồi, vậy đã đủ chống SSRF chưa?
Chưa đủ. Blocklist theo chuỗi rất dễ vượt:
http://2130706433/ (127.0.0.1 dạng số)
http://[::1]/ (IPv6 loopback)
http://169.254.169.254/latest/meta-data/ (metadata cloud: lấy được credential!)
http://evil.example/ -> 302 -> http://10.0.0.5/admin (redirect)
http://a.evil.example DNS lúc check = IP public, lúc tải = 10.0.0.5 (DNS rebinding)
Cách làm đúng:
- Chỉ cho
https, port 443. - Resolve DNS một lần, kiểm tra IP không thuộc dải private, loopback, link-local (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, ::1, fc00::/7), rồi kết nối đúng IP đó.
- Không follow redirect (hoặc kiểm tra lại từng bước).
- Giới hạn kích thước, timeout, Content-Type là ảnh.
- Tốt nhất: chạy downloader ở một service/egress proxy riêng không truy cập mạng nội bộ; bật IMDSv2 trên cloud.
Hoặc đổi thiết kế: seller upload trực tiếp lên object storage bằng presigned URL, server không phải tải gì.
Mình bind thẳng @RequestBody User user rồi repo.save(user) cho nhanh. Field role client không gửi thì đâu có sao?
Client bình thường không gửi, nhưng attacker thì gửi. Jackson bind mọi field có setter/constructor khớp tên, nên:
PATCH /me {"displayName":"An","role":"ADMIN","walletBalance":99999999}
↓ Jackson
User{displayName=An, role=ADMIN, walletBalance=99999999}
↓ repo.save
Thành admin, có tiền.
Ngược lại cũng nguy hiểm: field role client không gửi sẽ là null, save có thể ghi đè null vào DB.
Luôn dùng request DTO riêng chỉ có field được sửa, map tường minh sang entity, và response DTO riêng. @JsonIgnore trên entity là vá tạm, dễ sót khi thêm field mới.
Phỏng vấn hỏi về OWASP API Top 10 thì trả lời sao?
Q1. BOLA là gì, chống thế nào?
🇻🇳 Tiếng Việt:
BOLA là khi API nhận ID object từ client mà không kiểm tra object đó có thuộc user không, đổi ID là xem được dữ liệu người khác. Đây là mục số một. Em chống bằng cách đưa owner vào truy vấn, ví dụ
findByIdAndCustomerId, lấy user từ token, trả 404 nếu không khớp và có integration test cho mọi endpoint.
🇬🇧 English:
BOLA happens when an API takes an object ID from the client without checking that the object belongs to the caller, so changing the ID exposes other users' data. It is number one on the list. I prevent it by scoping queries to the owner, like
findByIdAndCustomerId, taking the user from the token, returning 404 on mismatch and covering every endpoint with integration tests.
Q2. BOLA, BFLA, BOPLA khác nhau?
🇻🇳 Tiếng Việt:
BOLA là thiếu kiểm tra ở mức object, BFLA ở mức chức năng như endpoint admin, BOPLA ở mức field, gồm mass assignment khi ghi và lộ field nhạy cảm khi đọc. Chống lần lượt bằng kiểm tra owner, deny-by-default theo role, và DTO allowlist cho input và output.
🇬🇧 English:
BOLA is missing authorization at the object level, BFLA at the function level such as admin endpoints, and BOPLA at the property level, which covers mass assignment on write and excessive data exposure on read. The fixes are ownership checks, deny-by-default role rules, and allowlisted DTOs for input and output.
Q3. Chống SSRF thế nào?
🇻🇳 Tiếng Việt:
Allowlist scheme và host, resolve DNS rồi chặn IP private, loopback, link-local như metadata 169.254.169.254, kết nối đúng IP đã kiểm tra, không follow redirect, giới hạn size và timeout. Tốt hơn là cô lập egress hoặc dùng presigned upload để server không phải fetch URL của user.
🇬🇧 English:
Allowlist schemes and hosts, resolve DNS and block private, loopback and link-local ranges such as the 169.254.169.254 metadata address, connect to the validated IP, disable redirects, and cap size and time. Even better, isolate egress or use presigned uploads so the server never fetches user-supplied URLs.
Cách nhớ ngắn gọn
- Ba câu hỏi phân quyền: object của ai (BOLA), chức năng của ai (BFLA), field của ai (BOPLA).
- DTO vào, DTO ra, không bind entity.
- Mọi thứ có giới hạn: page size, body, rate, quota, flow.
- URL của user và API của partner đều là input không tin cậy.
Tự kiểm tra
Dùng UUID cho order ID đã chống được BOLA chưa?
Chưa. Chỉ khó đoán hơn; ID vẫn có thể lộ qua log, link chia sẻ. Phải kiểm tra owner.
Response /users/me có passwordHash thuộc mục nào?
API3 BOPLA (excessive data exposure ở mức field).
Bot gom hết voucher thuộc mục nào?
API6 Unrestricted Access to Sensitive Business Flows.
Vì sao 169.254.169.254 nguy hiểm trong SSRF?
Là metadata endpoint của cloud, có thể trả credential tạm của instance.
/v1 cũ không ai quản lý vẫn chạy thuộc mục nào?
API9 Improper Inventory Management.