# Rà soát API theo OWASP API Top 10 trước khi đội bảo mật của khách vào cuộc

> Lỗ hổng đứng đầu danh sách OWASP có thể bị khai thác chỉ bằng cách đổi ID trong request, và bạn hoàn toàn có thể tự kiểm tra điều đó trước khi đội bảo mật của khách làm.

Bản gốc: https://fdetimes.net/vi/bach-khoa/owasp-api-top-10-ra-soat-truoc-ban-giao/

Một request `GET /api/v1/orders/1002`, gửi bằng token của người dùng không sở hữu đơn hàng 1002, trả về 200 kèm đầy đủ địa chỉ giao hàng. Đó là toàn bộ cuộc tấn công.

OWASP xếp lỗi này, Broken Object Level Authorization (BOLA), ở vị trí số một trong API Security Top 10 2023, và mô tả cách khai thác đúng như vậy: kẻ tấn công chỉ cần thao túng ID của đối tượng trong request.

Với một FDE, tuần trước khi đội bảo mật của khách bắt đầu pentest hệ thống bạn vừa triển khai là lúc đáng lo nhất. Thử hình dung họ tìm ra BOLA trong mười phút đầu: mọi thảo luận về tính năng sẽ dừng lại, và niềm tin bạn xây suốt nhiều tuần cũng lung lay theo. Vậy thì hãy tự làm pentester trước họ một bước.

Hướng dẫn này đi qua một buổi rà soát thực tế: bạn sẽ có một file kiểm kê API, một test BOLA tự động, vài lệnh kiểm tra rate limit, thông báo lỗi và SSRF, cùng một danh sách việc cần làm với những API mà hệ thống của bạn gọi ra ngoài.

## Cần chuẩn bị gì trước khi bắt đầu?

Bạn cần quyền truy cập môi trường staging (tuyệt đối không thử trên production khi chưa được khách cho phép), hai tài khoản test thuộc hai người dùng hoặc hai tenant khác nhau, cùng `curl` và Python có thư viện `requests` và `pytest`. Mở sẵn mục lục OWASP API Security Top 10 2023 để đánh dấu từng mục, từ API1 đến API10, như một checklist.

Các đoạn code dưới đây chỉ để minh hoạ và đã được đơn giản hoá. Endpoint `/api/v1/orders/{id}`, biến `TOKEN_A`, `TOKEN_B` và host `staging.example.com` là giả định, bạn cần thay bằng hệ thống thật của khách.

## Bước 1: bạn có biết mình đang phơi ra bao nhiêu API không?

Với API9 Improper Inventory Management, OWASP khuyên bước đầu tiên là kiểm kê toàn bộ API host và ghi lại những thông tin quan trọng của từng host. Không ai bảo vệ được một endpoint mà mình không biết là nó tồn tại. Hãy tạo một file inventory ngay trong repo:

```yaml
# api-inventory.yaml (minh hoạ)
- host: api.example.com
env: production
versions: [v2]
auth: oauth2
- host: staging.example.com
env: staging
versions: [v1, v2]
data: anonymized
```

Sau đó soi lại từng dòng theo hai câu hỏi. Có phiên bản cũ nào vẫn đang chạy không? Curity nói thẳng rằng không được để phiên bản API cũ tiếp tục hoạt động. Host non-production nào đang chứa dữ liệu thật? OWASP khuyên tránh dùng dữ liệu production cho các bản triển khai non-production.

**Kiểm tra:** mỗi host đều có chủ sở hữu, môi trường và phiên bản được ghi rõ. Thử hình dung staging ở trên vẫn mở `v1` và `v1` không có kiểm tra phân quyền mới. Khi đó pentester chẳng cần tấn công `v2`, họ chỉ việc đi cửa sau.

## Bước 2: tráo ID và xem API có chặn không

Đây là bước quan trọng nhất. Bạn dùng token của user B để đọc một đối tượng thuộc về user A:

```bash
# Order 1001 thuộc user A
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $TOKEN_B" \
https://staging.example.com/api/v1/orders/1001
```

Kết quả mong đợi là 403 hoặc 404. Nếu nhận 200, bạn đã tìm ra BOLA. OWASP khuyến nghị viết test để đánh giá cơ chế phân quyền, vì vậy hãy biến lệnh tay ở trên thành test chạy được trong CI:

```python
# test_bola.py (minh hoạ, đã đơn giản hoá)
import os, requests
BASE = "https://staging.example.com/api/v1"

def test_user_b_cannot_read_user_a_order():
r = requests.get(f"{BASE}/orders/1001",
headers={"Authorization": f"Bearer {os.environ['TOKEN_B']}"})
assert r.status_code in (403, 404)
```

Hãy để ý con số 1001. Nếu ID tăng tuần tự thì 1002, 1003 là những thứ ai cũng đoán được, nên OWASP khuyên dùng ID ngẫu nhiên, khó đoán. Dù vậy, ID khó đoán chỉ là lớp thứ hai.

Curity nhấn mạnh rằng kiểm tra phân quyền cấp đối tượng phải có mặt ở mọi hàm truy cập nguồn dữ liệu, còn OWASP REST Security Cheat Sheet yêu cầu dịch vụ không công khai kiểm soát truy cập ở từng endpoint.

**Điểm mấu chốt:** Một endpoint đã kiểm tra token nhưng không kiểm tra token đó có sở hữu đối tượng hay không vẫn là một endpoint chưa được bảo vệ.

**Kiểm tra:** lặp lại test cho mọi endpoint có `{id}` trong đường dẫn, kể cả `PUT` và `DELETE`.

## Bước 3: API key có đang gánh quá nhiều không?

Mở phần cấu hình xác thực ra và tìm những endpoint chỉ được bảo vệ bằng một API key tĩnh. Cheat Sheet của OWASP cảnh báo không được chỉ dựa vào API key để bảo vệ tài nguyên nhạy cảm, quan trọng hoặc có giá trị cao.

Nếu endpoint xuất dữ liệu khách hàng chỉ cần một header `X-API-Key`, hãy đánh dấu nó vào báo cáo của bạn trước khi đội bảo mật làm việc đó.

Cũng trong bước này, bạn rà luôn cấu hình. Curity khuyên không nên dựa vào cấu hình mặc định. Cách làm thực tế là mở từng file config của framework, gateway và server, rồi với mỗi giá trị chưa ai chủ động đặt, hỏi xem nó có thật sự cần bật trên môi trường của khách hay không, như thể bạn đang review code của người khác.

## Bước 4: lỗi trả về có đang kể quá nhiều không?

Gửi một request hỏng có chủ đích rồi xem response:

```bash
curl -s -H "Authorization: Bearer $TOKEN_A" \
-H "Content-Type: application/json" \
-d '{"quantity": "abc"}' \
https://staging.example.com/api/v1/orders
```

Cheat Sheet yêu cầu trả thông báo lỗi chung chung và tránh tiết lộ chi tiết thất bại khi không cần thiết. Nếu response chứa stack trace, tên bảng hay đường dẫn file trên server, bạn đã đưa cho pentester một tấm bản đồ.

Câu lệnh trên còn kiểm tra luôn chuyện validate: Bright Security coi validate và làm sạch đầu vào là tuyến phòng thủ đầu tiên, vì vậy `"abc"` cần bị schema chặn từ cửa với một lỗi 400 gọn gàng.

## Bước 5: gửi dồn dập và đếm số 429

Cheat Sheet khuyên trả về `429 Too Many Requests` khi request đến quá nhanh. Hãy thử trên endpoint đăng nhập, nơi brute-force hay nhắm tới:

```bash
for i in $(seq 1 50); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://staging.example.com/api/v1/login \
-d '{"user":"test","password":"wrong"}'
done | sort | uniq -c
```

Kết quả `50 401` có nghĩa là không có giới hạn nào, kẻ tấn công cứ thế thử mật khẩu mãi. Kết quả bạn muốn thấy là một vài 401 rồi chuyển sang 429. Bright Security mô tả API gateway như một điểm vào duy nhất cho toàn bộ lưu lượng API, nên nếu khách đã có gateway thì đó thường là nơi đặt rate limit hợp lý nhất.

Họ cũng khuyên đưa scan tự động vào CI/CD, dù nên nhớ đây là lời khuyên từ một nhà cung cấp công cụ DAST.

## Bước 6: còn những API mà hệ thống của bạn gọi ra ngoài thì sao?

API7 là Server Side Request Forgery (SSRF) và API10 là Unsafe Consumption of APIs, hai mục rất dễ bị bỏ sót nếu bạn chỉ nhìn vào endpoint của chính mình.

Với SSRF, hãy tìm mọi tham số nhận URL, chẳng hạn webhook hoặc "import từ link", vì đó là nơi server của bạn có thể bị lừa gọi vào mạng nội bộ của khách. Phép thử đơn giản nhất là đưa vào một URL trỏ về địa chỉ nội bộ.

Endpoint `/webhooks` và địa chỉ `10.0.0.5` dưới đây là giả định, hãy thay bằng một host nội bộ có thật mà khách cho phép bạn thử:

```bash
# Minh hoạ: đăng ký webhook trỏ vào một host nội bộ
curl -s -w "\n%{http_code}\n" \
-H "Authorization: Bearer $TOKEN_A" \
-H "Content-Type: application/json" \
-d '{"url": "http://10.0.0.5:8080/admin"}' \
https://staging.example.com/api/v1/webhooks
```

Kết quả bạn muốn thấy là API từ chối ngay với lỗi 400 vì URL không hợp lệ.

Nếu API nhận URL, rồi khi kích hoạt webhook bạn thấy server thật sự gọi vào host đó, hoặc response lộ ra nội dung từ trang nội bộ, bạn đã tìm ra một đường SSRF. **Kiểm tra:** lặp lại với `http://localhost` và với mọi tham số nhận URL mà bạn ghi được ở file inventory.

Với API10, Curity chỉ ra rằng developer thường tin dữ liệu từ API bên thứ ba hơn cả input của người dùng. Thử hình dung hệ thống của bạn lấy tỷ giá hoặc thông tin vận chuyển từ một đối tác rồi ghi thẳng vào database. Nếu đối tác bị xâm nhập, dữ liệu độc sẽ đi vào hệ thống mà không gặp bất kỳ lớp kiểm tra nào.

Cách xử lý là validate response của bên thứ ba bằng schema, đúng như cách bạn validate body ở Bước 4.

## Ba lỗi nhỏ nối lại thành một sự cố lớn thế nào?

Sáu bước trên dễ khiến bạn nghĩ mỗi mục OWASP là một ô tách biệt. Thử hình dung một tình huống: staging vẫn chạy `v1` (lỗi kiểm kê, API9), `v1` không kiểm tra quyền sở hữu đơn hàng (BOLA, API1), và staging được nạp bản sao dữ liệu production để test cho "giống thật".

Đứng riêng, mỗi lỗi nghe có vẻ nhẹ: một phiên bản cũ, một môi trường không phải production, một endpoint ít người dùng. Ghép lại, chỉ một vòng lặp đổi ID từ 1001 trở đi trên `/api/v1/orders` của staging là đủ để đọc địa chỉ thật của khách hàng thật. Thiếu thêm rate limit như ở Bước 5, vòng lặp đó chạy hết mà không bị chặn.

Vì thế khi ghi kết quả rà soát, đừng chỉ đánh dấu từng ô. Với mỗi lỗi tìm được, hãy hỏi thêm: lỗi này mở đường cho lỗi nào khác, và gỡ mắt xích nào thì cả chuỗi đứt? Trong ví dụ trên, tắt `v1` hoặc xoá dữ liệu thật khỏi staging đã đủ đứt chuỗi ngay cả trước khi bạn sửa code phân quyền.

## Những lỗi hay gặp khi tự rà soát

Lỗi phổ biến nhất là chỉ test bằng một tài khoản. Dùng một tài khoản thì không bao giờ phát hiện được BOLA, vì người dùng đó luôn sở hữu dữ liệu mình đọc. Lỗi thứ hai là chỉ rà phiên bản mới nhất trong khi `v1` vẫn chạy trên staging với dữ liệu thật.

Lỗi thứ ba là coi ID kiểu UUID như một biện pháp phân quyền, trong khi nó chỉ làm việc đoán ID khó hơn.

## Kỹ năng này xuất hiện thế nào ở site khách?

Nếu khách yêu cầu đội bảo mật của họ rà soát trước khi cho hệ thống lên production, một FDE gửi kèm file inventory, bộ test BOLA đang chạy xanh trong CI và một bảng đối chiếu mười mục OWASP ghi rõ "đã kiểm tra, bằng cách nào" có thể biến buổi pentest từ một cuộc săn lỗi thành một buổi xác nhận.

Khi đọc JD, hãy để ý những cụm như "work with customer security teams" hay "security review"; còn trong CV, hãy viết cụ thể kiểu "viết test phân quyền cấp đối tượng cho N endpoint, chặn lỗi BOLA trước khi pentest", thay vì chỉ ghi "am hiểu OWASP".

Đội bảo mật của khách sẽ đọc OWASP Top 10 dù bạn có đọc hay không. Điều duy nhất bạn chọn được là ai tìm ra lỗi trước.

**Thử ngay tuần này:**

- Lấy một endpoint dạng /resource/&#123;id&#125; trong dự án hiện tại, tạo hai tài khoản test và viết một test khẳng định user B nhận 403 hoặc 404 khi đọc dữ liệu của user A.
- Liệt kê mọi host và phiên bản API đang chạy vào một file inventory, đánh dấu host nào là non-production nhưng đang chứa dữ liệu thật.
- Gửi 50 request liên tiếp vào endpoint đăng nhập trên staging và đếm xem có bao nhiêu response 429.

## Nguồn

- [OWASP API Security Top 10 2023 – Table of Contents](https://api-security.owasp.org/editions/2023/en/0x00-toc)

- [API1:2023 Broken Object Level Authorization – OWASP API Security](https://api-security.owasp.org/editions/2023/en/0xa1-broken-object-level-authorization)

- [API9:2023 Improper Inventory Management – OWASP API Security](https://api-security.owasp.org/editions/2023/en/0xa9-improper-inventory-management)

- [Top 10 API Security Vulnerabilities (Curity)](https://curity.io/resources/learn/owasp-top-ten/)

- [Top API Vulnerabilities and 6 Ways to Mitigate Them (Bright Security)](https://brightsec.com/blog/top-api-vulnerabilities-and-6-ways-to-mitigate-them/)

- [REST Security Cheat Sheet – OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html)
