Tóm tắt: Đầu vào API của bạn là một bề mặt tấn công, vì vậy hãy kiểm tra nó như một bề mặt tấn công. Viết các trường hợp kiểm thử tiêu cực (negative cases) gửi các trường có kích thước quá lớn, sai kiểu dữ liệu, các body bị định dạng sai và các chuỗi tấn công (injection strings), sau đó xác nhận rằng endpoint trả về lỗi 4xx chứ không bao giờ là 5xx. Biến việc xác thực schema thành một kiểm soát bảo mật với additionalProperties: false, enums và giới hạn độ dài. Chạy toàn bộ bộ kiểm thử trong CI trên mỗi lần thay đổi. Các tác nhân AI làm cho điều này trở nên cấp bách: chúng tạo và chuyển tiếp các payload với tốc độ máy, vì vậy việc "tải dữ liệu này" lặng lẽ trở thành "chạy mã này" giờ đây có thể mở rộng quy mô.Hầu hết các bộ kiểm thử chỉ chứng minh API của bạn hoạt động khi người gọi lịch sự. Bạn gửi một body hợp lệ, bạn nhận được 200, xác nhận vượt qua. Kết quả đó hầu như không cho bạn biết điều gì xảy ra khi body không thân thiện. Đầu vào không đáng tin cậy là bất kỳ dữ liệu nào mà endpoint của bạn không tự tạo ra: request bodies, query strings, headers, file uploads, webhook payloads và JSON mà tác nhân AI tự động lắp ráp. Tất cả đều đáng được giả định giống nhau, đó là sẽ có ai đó cuối cùng gửi phiên bản tồi tệ nhất có thể của nó.
Vào tháng 7 năm 2026, Hugging Face đã mô tả một sự cố bảo mật mà vector tấn công là dữ liệu, chứ không phải mật khẩu bị đánh cắp. Chúng tôi đã đề cập riêng về các bài học từ vụ vi phạm đó; hướng dẫn này là phần thực hành. Bạn sẽ xây dựng các kiểm thử gửi loại đầu vào mà kẻ tấn công gửi, sau đó chạy chúng tự động trên mỗi lần thay đổi. Các danh mục này phù hợp với OWASP API Security Top 10, một tài liệu đáng để giữ mở trong một tab. Apidog là một cách để thiết kế hợp đồng và thực hiện các kiểm thử này, nhưng các ý tưởng này vẫn có giá trị trong bất kỳ framework nào bạn đã sử dụng.
Đầu vào là một bề mặt tấn công, không phải là một trường biểu mẫu
Xác thực thường được coi là sự lịch sự trong trải nghiệm người dùng: bắt lỗi email trống, hiển thị đường viền màu đỏ, sau đó bỏ qua. Cách hiểu đó chính là vấn đề. Mỗi trường mà API của bạn chấp nhận là một lời hứa mà người gọi có thể phá vỡ, và mỗi lời hứa bị phá vỡ là một con đường dẫn vào logic của bạn. Một tham số limit mà bạn mong đợi là một số nguyên nhỏ trở thành 999999999. Một filename mà bạn mong đợi là một từ đơn lẻ trở thành ../../etc/passwd. Một đối tượng config mà bạn mong đợi chứa các cài đặt trở thành một tập hợp các lệnh.
Kiểm thử bảo mật không phải là một lĩnh vực riêng biệt được thêm vào cuối cùng. Nó là cùng một phương pháp kiểm thử tiêu cực mà bạn đã biết, nhắm vào các trường có khả năng gây hại nhất cho bạn. Nếu bạn xây dựng thói quen hỏi "điều tồi tệ nhất có thể xảy ra trong trường này là gì?", bạn đã đi được phần lớn quãng đường đến các thực hành trong hướng dẫn thực tiễn tốt nhất về bảo mật API của chúng tôi. Phần còn lại của bài viết này sẽ biến câu hỏi đó thành các kiểm thử cụ thể mà bạn có thể chạy.
Làm thế nào "tải dữ liệu này" trở thành "chạy mã này"
Sự cố Hugging Face là một ví dụ rõ ràng về lý do tại sao đầu vào cần được chú ý. Hugging Face cho biết vector tấn công là các tập dữ liệu độc hại: một tập dữ liệu được chế tạo đã kích hoạt một bộ tải tập dữ liệu mã từ xa, và một injection template tồn tại trong cấu hình tập dữ liệu. Bạn có thể đọc báo cáo của công ty trong báo cáo sự cố bảo mật của họ.
Hãy xem xét hình thức của thất bại đó. Một endpoint đã chấp nhận một thứ được mô tả là dữ liệu. Việc tải dữ liệu đó đã chạy một đường dẫn mã có thể thực thi các lệnh do kẻ tấn công kiểm soát. "Tải dữ liệu này" đã trở thành "chạy mã này." Injection template cũng là một câu chuyện tương tự ở quy mô nhỏ hơn: một giá trị cấu hình được cho là văn bản tĩnh đã được đánh giá, vì vậy văn bản đã trở thành thực thi.
Bài học rút ra không phải là "Hugging Face đã mắc một sai lầm hiếm gặp." Mà là bất kỳ endpoint nào chấp nhận tên bộ tải, định dạng, template, đối tượng được tuần tự hóa hoặc blob cấu hình đều đang chấp nhận các lệnh, dù bạn có chủ ý hay không. Nếu bạn chưa bao giờ viết một kiểm thử gửi cấu hình độc hại đến endpoint đó, bạn chưa bao giờ thực sự kiểm tra giả định rằng nó vẫn tĩnh. Giả định không được kiểm tra đó chính là toàn bộ lỗ hổng.
Xác thực Schema như một kiểm soát bảo mật
Kiểm soát rẻ nhất bạn có thể thêm là một schema nghiêm ngặt ở biên. Một schema không chỉ là tài liệu. Khi bạn từ chối bất kỳ thứ gì không khớp, schema trở thành một bộ lọc chạy trước khi logic kinh doanh của bạn nhìn thấy yêu cầu. JSON Schema cung cấp cho bạn các yếu tố cơ bản để làm cho bộ lọc đó chặt chẽ.
Dưới đây là một schema cho cấu hình tập dữ liệu từ câu chuyện, được viết sao cho hầu hết các đầu vào độc hại không bao giờ đến được mã ứng dụng:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["loader", "name"],
"properties": {
"loader": { "enum": ["csv", "json", "parquet"] },
"name": { "type": "string", "maxLength": 128, "pattern": "^[\\w .-]+$" },
"rows": { "type": "integer", "minimum": 0, "maximum": 1000000 }
}
}
Hãy đọc nó như bốn biện pháp phòng thủ riêng biệt. additionalProperties: false từ chối thẳng một trường template bị tuồn vào, vì vậy kẻ tấn công không thể thêm một trường nào. Enum loader có nghĩa là pickle:// hoặc bất kỳ bộ tải mã từ xa nào đơn giản không phải là giá trị hợp lệ. maxLength ngăn chặn chuỗi nhiều megabyte nhằm mục đích làm cạn kiệt bộ nhớ. pattern trên name từ chối các ký tự {{ và '; DROP TABLE trước khi chúng đi xa hơn. Không dòng nào trong số này biết về kẻ tấn công. Chúng chỉ chấp nhận tập hợp đầu vào hẹp mà bạn thực sự hỗ trợ, và sự hẹp hòi đó chính là thuộc tính bảo mật.
Xác thực hợp đồng như thế này không bắt được mọi lỗ hổng, và không schema nào có thể. Điều nó giải quyết là một loại cụ thể và phổ biến: lỗi "chúng tôi chưa bao giờ kiểm tra endpoint này chấp nhận những gì". Loại đó là nơi bắt đầu của một số lượng đáng ngạc nhiên các vụ vi phạm.
Kiểm thử tiêu cực: chứng minh endpoint từ chối
Các kiểm thử happy-path xác nhận rằng đầu vào tốt tạo ra đầu ra tốt. Các kiểm thử tiêu cực xác nhận rằng đầu vào xấu tạo ra sự từ chối được kiểm soát. Sự khác biệt này quan trọng vì sự từ chối là một tính năng: một lỗi 400 với thông báo rõ ràng là API của bạn đang bảo vệ ranh giới của nó. Một lỗi 500 là API của bạn đang mất kiểm soát.
Xây dựng các trường hợp tiêu cực theo cùng một cách mọi lúc. Đối với mỗi trường, hãy viết ra những gì nó phải từ chối: sai kiểu, thiếu khi bắt buộc, có mặt khi bị cấm, quá dài, ngoài phạm vi và các chuỗi injection phù hợp với định dạng của nó. Sau đó xác nhận hai điều trên phản hồi. Thứ nhất, trạng thái là 4xx, thường là 400 hoặc 422. Thứ hai, trạng thái không bao giờ là 5xx. Một lỗi 500 có nghĩa là đầu vào độc hại của bạn đã đến được mã chưa sẵn sàng cho nó, đó chính xác là khả năng tiếp cận mà kẻ tấn công muốn. Danh sách kiểm tra kiểm thử bảo mật API của chúng tôi có một danh sách bắt đầu theo từng trường mà bạn có thể điều chỉnh.
Một quy tắc giữ cho điều này trung thực: xác nhận trên hành vi, không phải trên văn bản lỗi. Nếu bạn xác nhận rằng thông báo có nội dung "invalid loader," một thay đổi mã vô hại sẽ làm hỏng kiểm thử của bạn và dạy nhóm nới lỏng nó. Hãy xác nhận mã trạng thái, và nếu có thể, xác nhận rằng không có tác dụng phụ nào xảy ra.
Các loại injection đáng được kiểm thử chuyên biệt
Một vài loại injection xuất hiện đủ thường xuyên để mỗi loại xứng đáng có các trường hợp kiểm thử cố định, chứ không phải là một kiểm tra thủ công một lần. Bạn không cần phải kiểm tra cạn kiệt ở đây. Bạn cần một trường hợp thăm dò cho mỗi loại để một lỗi hồi quy sẽ thất bại một cách rõ ràng. Các công cụ chạy phát hiện lỗ hổng API tự động có thể mở rộng phạm vi bao phủ sau này, nhưng một vài trường hợp được viết thủ công sẽ phát hiện các lỗ hổng rõ ràng trước tiên.
- SQL injection: gửi
1); DROP TABLE datasets;--vào bất kỳ trường nào đến một truy vấn. Endpoint nên coi nó là một giá trị chữ và trả lời 400, hoặc trả về một kết quả trống, và không bao giờ hiển thị lỗi cơ sở dữ liệu. - Template injection: gửi
{{ 7*7 }}và{{ config.__class__ }}vào các trường tên và nhãn. Nếu phản hồi bao giờ chứa49, một công cụ template đã đánh giá đầu vào của bạn, và đó là mã từ xa đang chờ được thực thi. - Chống tuần tự hóa không an toàn và bộ tải mã từ xa: gửi một
loaderlàpickle://hoặc một đối tượng được tuần tự hóa vào nơi mà một giá trị đơn giản thuộc về. Đây chính xác là hình thức của Hugging Face. Endpoint nên từ chối các bộ tải không xác định bằng danh sách cho phép (allowlist), chứ không nên cố gắng "hữu ích". - Command injection: gửi
; idvà$(id)vào bất kỳ trường nào có thể trở thành một đối số shell, chẳng hạn như tên tệp hoặc tùy chọn chuyển đổi. Một phản hồi 200 làm lộ ID người dùng là một phát hiện quan trọng, không phải là một điều kỳ lạ.
Payload quá lớn, định dạng sai và nhầm lẫn Content-Type
Không phải mọi đầu vào độc hại đều là một chuỗi thông minh. Một số đơn giản là quá lớn hoặc có hình dạng sai, và những điều này thường làm hỏng trình phân tích trước khi logic xác thực của bạn kịp chạy.
Gửi một payload quá lớn: một trường duy nhất chứa năm megabyte một ký tự, hoặc một mảng JSON với một triệu phần tử. Một API lành mạnh sẽ thực thi giới hạn kích thước body và trả về 413 thay vì cấp phát bộ nhớ cho đến khi sập. Cũng gửi các body bị định dạng sai: JSON bị cắt, một dấu phẩy thừa ở cuối, hoặc JSON được lồng hàng nghìn cấp độ sâu để thăm dò tình trạng cạn kiệt stack. Câu trả lời đúng là một lỗi 400 nhanh chóng, chứ không phải là một worker bị treo.
Nhầm lẫn Content-Type là một vấn đề âm thầm. Khai báo Content-Type: application/json nhưng gửi XML, hoặc khai báo application/xml và gửi một payload với một thực thể bên ngoài để thăm dò XXE. Đảo ngược lại và gửi JSON dưới dạng text/plain để xem liệu một trình phân tích lỏng lẻo có chấp nhận nó không. Mỗi sự không khớp kiểm tra xem máy chủ của bạn có tin tưởng header, tin tưởng body hay kiểm tra xem hai cái có khớp nhau không. Nó phải yêu cầu sự đồng ý trước khi phân tích bất kỳ thứ gì.
Tại sao tác nhân AI làm tăng mức độ rủi ro
Mọi điều trên đều đúng trước khi các tác nhân tồn tại. Các tác nhân thay đổi khối lượng và tốc độ. Một kẻ tấn công là con người gõ từng yêu cầu độc hại một. Một tác nhân AI tạo và chuyển tiếp các payload với tốc độ máy, và nó sẽ vui vẻ xây dựng các đầu vào mà một người có thể không bao giờ bận tâm thử.
Ba thuộc tính làm cho điều này tồi tệ hơn. Các tác nhân tổng hợp đầu vào, vì vậy chúng tạo ra các giá trị trường mà không con người nào viết và không kiểm thử nào lường trước. Các tác nhân thử lại và chuỗi các cuộc gọi, vì vậy một tài liệu thượng nguồn bị nhiễm độc có thể biến thành hàng nghìn yêu cầu độc hại chống lại endpoint của bạn trong vài giây. Và các tác nhân chuyển tiếp dữ liệu mà chúng được yêu cầu tin tưởng, đó là cách một payload ẩn trong tập dữ liệu hoặc webhook trở thành một yêu cầu thực sự đối với API của bạn. Mô hình Hugging Face, nơi "tải dữ liệu này" biến thành "chạy mã này", chính xác là loại lệnh mà một tác nhân sẽ thực hiện qua ranh giới tin cậy mà không hề hay biết. Lưu ý của chúng tôi về prompt injection cho các nhóm API đi sâu hơn về việc chuyển giao đó. Biện pháp phòng thủ không thay đổi; nó chỉ cần phải tự động, bởi vì bạn không thể xem xét lưu lượng tác nhân bằng tay.
Xây dựng bộ kiểm thử tiêu cực và chạy nó trong CI trên mỗi lần thay đổi
Biến các trường hợp trên thành một bộ kiểm thử chạy trên mỗi pull request. Dưới đây là một phiên bản tham số hóa nhỏ gọn trong pytest, tấn công một endpoint staging và xác nhận sự từ chối được kiểm soát:
import httpx
import pytest
BASE = "https://staging.internal/v1"
HOSTILE_CONFIGS = [
{"loader": "pickle://s3/models/payload.pkl", "format": "auto"}, # remote-code loader
{"loader": "csv", "name": "{{ 7*7 }}"}, # template injection
{"loader": "csv", "name": "{{ config.__class__ }}"}, # object traversal
{"loader": "csv", "filter": "1); DROP TABLE datasets;--"}, # SQL injection
{"loader": "csv", "name": "A" * 5_000_000}, # oversized field
]
@pytest.mark.parametrize("config", HOSTILE_CONFIGS)
def test_dataset_config_is_refused(config):
r = httpx.post(f"{BASE}/datasets", json={"config": config}, timeout=10)
assert r.status_code in (400, 413, 422), r.text # a boundary that says no
assert r.status_code < 500, "5xx means the payload reached logic it should not"
assert "49" not in r.text, "template rendered: server-side template injection"
Kết nối nó vào CI để nó kiểm soát các lần hợp nhất. Một tác vụ GitHub Actions tối thiểu sẽ thực hiện công việc:
name: api-abuse-tests
on: [push, pull_request]
jobs:
negative-input:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pytest tests/negative_input.py -q
Đây là lúc một công cụ schema-first khẳng định vị trí của nó. Trong Apidog, bạn thiết kế endpoint theo một hợp đồng OpenAPI, vì vậy mỗi yêu cầu và phản hồi được kiểm tra dựa trên hợp đồng đó trong khi bạn kiểm thử. Bạn có thể lưu các kịch bản tiêu cực ngay bên cạnh các kịch bản happy-path: các trường quá lớn, sai kiểu, và các chuỗi injection ở trên, mỗi cái đều có một xác nhận rằng trạng thái là 4xx. Sau đó, bạn chạy các kịch bản tương tự trong CI thông qua Apidog CLI, vì vậy một thay đổi làm lỏng lẻo việc xác thực một cách âm thầm sẽ làm lỗi bản dựng thay vì được triển khai. Nếu bạn muốn thử, hãy Tải xuống Apidog và thêm một kịch bản tiêu cực vào một endpoint mà bạn đã có.
Hãy rõ ràng về ranh giới. Apidog là một công cụ thiết kế, kiểm thử, mô phỏng và tài liệu. Nó không chạy tường lửa ứng dụng web (WAF), lọc lưu lượng trực tiếp hoặc thay thế một SIEM, và việc xác thực hợp đồng trong quá trình kiểm thử sẽ không bắt được mọi lỗ hổng. Điều nó làm tốt là làm cho hợp đồng rõ ràng và giúp bạn trung thực về những gì một endpoint chấp nhận, để danh mục "chúng tôi chưa bao giờ kiểm tra" không còn là điều làm bạn bất ngờ trong môi trường sản xuất.
Các câu hỏi thường gặp
Sự khác biệt giữa kiểm thử tiêu cực và fuzzing là gì? Kiểm thử tiêu cực gửi một tập hợp các đầu vào xấu đã được chọn có chủ đích, một trường hợp cho mỗi lỗi mà bạn quan tâm. Fuzzing gửi số lượng lớn các đầu vào ngẫu nhiên hoặc đã biến đổi để tìm các trường hợp mà bạn không nghĩ đến. Bắt đầu với các kiểm thử tiêu cực vì chúng nhanh, xác định và dễ chạy trong CI. Thêm fuzzing khi bạn muốn phạm vi bao phủ rộng hơn trí tưởng tượng của mình.
Các kiểm thử này có nên chạy trên môi trường sản xuất không? Không. Hãy chạy chúng trên môi trường staging hoặc một môi trường biệt lập. Một số trường hợp, như payload quá lớn hoặc thăm dò injection lệnh, được thiết kế để gây áp lực lên hệ thống, và một vài trường hợp có thể làm thay đổi dữ liệu nếu có lỗi. Một môi trường kiểm thử chuyên dụng cho phép các kiểm thử hoạt động mạnh mẽ mà không có bất kỳ rủi ro nào đối với người dùng thực.
Tường lửa hoặc WAF sẽ bắt được điều này phải không? WAF là một lớp phòng thủ theo chiều sâu hữu ích, nhưng nó không thể thay thế việc ứng dụng từ chối đầu vào xấu. Các quy tắc có thể bị bỏ qua, và WAF không thể biết logic kinh doanh của bạn. Mục đích của các kiểm thử này là để chứng minh bản thân endpoint từ chối, vì vậy bạn không phải dựa vào một bộ lọc mà bạn không kiểm soát hoàn toàn.
Bao nhiêu trường hợp tiêu cực là đủ cho mỗi endpoint? Hãy nhắm đến một trường hợp cho mỗi trường cho mỗi loại lỗi mà nó có thể gặp phải: sai kiểu, ngoài phạm vi, quá dài, trường bị cấm và bất kỳ chuỗi injection nào phù hợp với định dạng của nó. Đó thường là một vài trường hợp cho mỗi endpoint, chứ không phải hàng trăm. Phạm vi bao phủ của các loại quan trọng hơn số lượng thô.
Xác thực schema có ngăn chặn hoàn toàn injection không? Không, và nó không nên là lớp duy nhất của bạn. Một schema nghiêm ngặt loại bỏ một phần lớn đầu vào bị định dạng sai và quá lớn, đồng thời chặn các trường không mong muốn, nhưng một giá trị có thể hợp lệ theo schema và vẫn là một SQL injection hoặc template injection. Duy trì các truy vấn tham số hóa, deserialization an toàn và mã hóa đầu ra, đồng thời sử dụng schema để thu hẹp bề mặt mà các lớp đó phải bảo vệ.
