API 的输入就是攻击面,请务必以此为前提进行测试。编写发送超大字段、错误类型、畸形 body 和注入字符串的异常测试用例,然后断言接口返回 4xx 响应而绝不返回 5xx。通过使用 additionalProperties: false、枚举和长度限制,将数据模型验证转化为安全控制手段。在每次代码变更时,在 CI 中运行整个测试套件。AI Agent 的出现让这项任务变得刻不容缓:它们能以机器的速度生成和转发 payload,这使得“加载这些数据”在不知不觉中演变成“运行这段代码”的安全风险被无限放大。大多数测试套件只能证明在调用方行为规范时 API 能够正常工作。你发送一个有效的 body,获得 200 响应,断言通过。然而,这一结果几乎无法告诉你当 body 具有恶意时会发生什么。不可信输入是指任何并非由接口自身生成的数据:请求 body、query 参数、header、文件上传、webhook payload 以及 AI Agent 动态组装的 JSON。所有这些数据都应该基于同一个假设:迟早会有人发送可能的最坏版本。
在 2026 年 7 月,Hugging Face 披露了一起安全事件,其攻击入口是数据,而不是被盗的密码。我们已经在其他文章中单独总结了那次泄露事件的教训;本指南则是实操部分。你将构建用于发送攻击者输入数据的测试,并在每次变更时自动运行它们。这些分类与 OWASP API Security Top 10 一一对应,建议你在浏览器中打开此链接备用。Apifox 是设计契约并驱动这些测试的一种方式,但这些核心理念在你已经使用的任何框架中同样适用。
输入是攻击面,而非表单字段
输入验证常常被看作是出于用户体验考虑的一种礼貌行为:捕捉空白的邮箱地址、显示红框,然后继续。这种定势思维正是问题所在。你的 API 接收的每一个字段都是调用方可能会打破的承诺,而每一个被打破的承诺都是一条深入你代码逻辑的通道。你预期为较小整数的 limit parameter 变成了 999999999。你预期是单个单词的 filename 变成了 ../../etc/passwd。你预期用于保存设置的 config object 变成了一组执行指令。
安全测试并不是在最后阶段才拼凑上去的独立学科。它就是你所熟知的异常测试,只是目标对准了最容易对你造成伤害的字段。如果你养成了经常思考“这个字段中能填入的最坏数据是什么”的习惯,那么你已经基本掌握了我们 API 安全最佳实践指南中的核心做法。本文的其余部分将把这一个问题转化为你可以运行的具体测试。
“加载这些数据”是如何变成“运行这段代码”的
Hugging Face 事件是一个典型的例子,生动地说明了为什么输入需要受到如此关注。Hugging Face 表示,入口向量是恶意数据集:精心设计的数据集触发了远程代码数据集加载器,并且在数据集配置中存在模板注入。您可以在该公司的安全事件报告中阅读他们自己的描述。
仔细思考这种失败形式。一个接口接受了被描述为数据的内容。加载这些数据运行了一条可以执行攻击者控制的指令的代码路径。“加载此数据”变成了“运行此代码”。模板注入在较小尺度上也是同样的故事:本应是静态文本的配置值被执行了评估,因此文本变成了执行。
这一事件的启示并不是“Hugging Face 犯了一个罕见的错误”,而是任何接受加载器名称、格式、模板、序列化对象或配置块(config blob)的接口,无论你是否出于本意,实际上都在接受指令。如果你从未编写过向该接口发送恶意配置的测试,你就从未真正验证过它会保持静态的假设。这个未经验证的假设正是漏洞的全部所在。
作为安全控制的数据模型校验
你可以添加的最廉价的控制措施是在边缘应用严格的数据模型。数据模型不仅是文档。当你拒绝任何不匹配的内容时,数据模型就会变成一个过滤器,在你的业务逻辑看到请求之前运行。JSON Schema 为你提供了使该过滤器严密的原语。
以下是针对上述故事中数据集配置的数据模型,其编写方式确保了大多数恶意输入永远不会到达应用程序代码:
{
"$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 }
}
}
可以将其视为四道独立的防线。additionalProperties: false 会直接拒绝夹带的 template 字段,因此攻击者无法添加该字段。loader 枚举(enum)意味着 pickle:// 或任何远程代码加载器根本不是有效值。maxLength 终结了旨在耗尽内存的数兆字节字符串。name 上的 pattern 在 {{ 和 '; DROP TABLE 字符进一步传播之前就予以拒绝。这些代码行本身并不了解攻击者,它们只接受你实际支持的狭窄输入集,而这种狭窄性正是安全属性所在。
像这样的契约校验并不能捕获每一次漏洞利用,任何数据模型都做不到。它所封闭的是一个特定且常见的类别:即“我们从未检查过这个接口接受什么”的 Bug。这一类别正是惊人数量的数据泄露事件的起点。
负面测试:证明接口会拒绝
Happy-path 测试用于断言正确的输入会产生正确的输出。异常测试则用于断言错误的输入会产生受控的拒绝。这种区别至关重要,因为拒绝也是一种功能:返回清晰错误信息的 400 响应是你的 API 在捍卫其边界,而 500 响应则意味着你的 API 失去了对边界的控制。
每次都以相同的方式构建异常用例。对于每个字段,写下它必须拒绝的情况:类型错误、必填项缺失、禁用项存在、长度超限、数值越界,以及符合其格式的注入字符串。然后对响应进行两点断言。第一,状态码为 4xx,通常是 400 或 422。第二,状态码绝不能是 5xx。500 意味着你的恶意输入到达了尚未准备好处理它的代码,而这正是攻击者所期望的可达性。我们的 API 安全测试清单中有一个逐字段的初始列表,你可以直接套用。
有一条原则可以保证测试的真实性:针对行为进行断言,而不是针对错误文本。如果你断言错误信息必须为 “invalid loader”,那么一次无害的重构就可能会破坏你的测试,并导致团队倾向于放宽测试限制。请断言状态码,并在可行的情况下,断言完全没有产生任何副作用。
值得进行专项测试的注入类型
有几种注入类型非常常见,以至于每种类型都值得拥有常备的测试用例,而不是仅进行一次性的手动检查。你不必在此做到面面俱到。每种类型只需要一个探测用例,以便在发生回归时能清晰报错。后续可以使用运行自动化 API 漏洞检测的工具来扩大覆盖范围,但少数几个手写的用例就能首先捕获最明显的漏洞。
- SQL 注入:向任何会触发查询的字段发送
1); DROP TABLE datasets;--。接口应该将其视为字面值并返回 400,或者返回空结果,绝不能暴露数据库错误。 - 模板注入:向 name 和 label 字段发送
{{ 7*7 }}和{{ config.__class__ }}。如果响应中包含了49,则说明模板引擎解析了你的输入,这意味着随时可能发生远程代码执行。 - 不安全的反序列化和远程代码加载器:在原本应该传入纯文本值的地方,发送
pickle://格式的loader或序列化对象。这正是 Hugging Face 漏洞的典型特征。接口应该通过白名单拒绝未知的加载器,而不是试图提供不必要的帮助。 - 命令注入:向任何可能成为 shell 参数的字段(例如文件名或转换选项)发送
; id和$(id)。如果返回 200 响应并泄露了用户 id,这是一个严重的漏洞,而不仅仅是值得关注的奇特现象。
超大、格式错误以及 Content-Type 混淆
并非所有恶意输入都是精心构造的字符串。有些只是体积过大或格式不对,这些往往在你的验证逻辑运行之前就已经导致解析器崩溃了。
发送一个超大的 payload:例如一个包含 5MB 单一字符的字段,或者一个包含 100 万个元素的 JSON 数组。一个健康的 API 会强制执行 body 大小限制并返回 413,而不是不断分配内存直到崩溃。也可以发送格式错误的 body:截断的 JSON、尾随逗号,或者嵌套上千层的 JSON,以此来探测栈耗尽。正确的响应应当是快速返回 400,而不是挂起工作进程。
Content-Type 混淆是一种隐蔽的攻击方式。例如,声明 Content-Type: application/json 却发送 XML;或者声明 application/xml 并发送带有外部实体的 payload 以探测 XXE。反过来,将 JSON 作为 text/plain 发送,看看宽松的解析器是否仍会接受它。每一次不匹配都在测试你的服务端是信任 header、信任 body,还是校验两者是否一致。在解析任何内容之前,它应该要求两者必须一致。
为什么 AI Agent 会带来更高的风险
在 Agent 出现之前,上述一切就已经是事实了。但 Agent 改变了攻击的规模和速度。人类攻击者一次只能键入一个恶意请求。而 AI Agent 则能以机器般的速度生成并转发 payload,并且会乐此不疲地构建人类根本不会费心去尝试的输入。
三个特性使情况变得更加糟糕。Agent 会合成输入,因此它们会产生人类从未写过、测试也未曾预料到的字段值。Agent 会进行重试和链式调用,因此单个被污染的上游文档可能会在几秒钟内转化为针对你接口的数千个恶意请求。此外,Agent 会转发它们被告知要信任的数据,这就是隐藏在数据集或 webhook 中的 payload 如何变成针对你 API 的真实请求的。Hugging Face 模式(即“加载此数据”演变为“运行此代码”)正是 Agent 会在不知不觉中跨越信任边界执行的那类指令。我们针对 API 团队的提示词注入说明对这种交接进行了更深入的探讨。防御手段并没有改变,只是它必须是自动化的,因为你无法手动审查 Agent 产生的流量。
构建异常测试套件并在每次变更时在 CI 中运行
将上述用例转化为在每次 Pull Request 时运行的测试套件。以下是使用 pytest 编写的精简参数化版本,它会请求 staging 环境接口并断言其返回受控的拒绝响应:
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"
将其集成到 CI 中,以便在合并时进行拦截。一个极简的 GitHub Actions 工作流即可实现:
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
这正是数据模型优先工具的用武之地。在 Apifox 中,你基于 OpenAPI 契约设计接口,因此在测试时,每个请求和响应都会对照该契约进行校验。你可以将异常测试场景与正常流程的场景并排保存:超大字段、错误类型以及上述注入字符串,并为每个场景添加状态码为 4xx 的断言。然后,你可以通过 Apifox CLI 在 CI 中运行相同的测试场景,这样一旦有修改悄悄放宽了校验规则,构建就会失败,从而阻止其发布。如果你想尝试一下,请下载 Apifox 并为你现有的接口添加一个异常测试场景。
请明确其边界。Apifox 是一款设计、测试、mock 和文档工具。它不运行 Web 应用防火墙、不拦截实时流量,也无法替代 SIEM,而且测试过程中的契约校验也无法捕获所有漏洞利用。它所擅长的是让契约变得明确,并促使你严格把控接口所接受的内容,这样在生产环境中,你就不再会被“我们从未检查过”这类疏漏打得手忙脚乱。
常见问题解答
异常测试和模糊测试(Fuzzing)有什么区别? 异常测试会发送一组你特意挑选的精心设计的异常输入,每个输入对应一个你关注的故障。模糊测试则会发送大量随机或变异的输入,以发现你未曾料到的情况。建议从异常测试开始,因为它们速度快、结果确定,且易于在 CI 中运行。当你想在想象力之外拓展测试覆盖面时,再引入模糊测试。
这些测试应该在生产环境中运行吗? 不应该。请在预发布环境或隔离的环境中运行它们。某些用例(如超大负载或命令注入探测)旨在对系统施加压力,如果存在漏洞,其中少数用例还可能会修改数据。专属的测试环境可以让测试更具攻击性,而不会对真实用户造成任何风险。
防火墙或 WAF 难道不会拦截这些吗? WAF 是一种有用的深度防御手段,但它不能替代应用程序本身拒绝恶意输入。规则是可以被绕过的,而且 WAF 无法了解你的业务逻辑。这些测试的目的是证明接口本身能够拒绝这些输入,从而让你不必依赖一个你无法完全控制的过滤器。
每个接口需要多少个异常用例才足够? 目标是针对每个字段可能遭受的每种失效类别设计一个用例:类型错误、超出范围、过长、禁用字段以及任何符合其格式的注入字符串。对于每个接口,这通常只需要几个用例,而不是数百个。覆盖这些类别比单纯的用例数量更重要。
数据模型校验能完全阻止注入吗? 不能,而且它不应该是你唯一的防护层。严格的数据模型可以过滤掉大部分格式错误和超大的输入,并拦截未预期的字段,但一个值即使通过了数据模型校验,仍可能是 SQL 注入或模板注入。请继续保留参数化查询、安全反序列化和输出编码,并利用数据模型来缩小这些防护层必须防御的攻击面。
开发必备:API 全流程管理神器 Apifox
介绍完上文的内容,我想额外介绍一个对开发者同样重要的效率工具 —— Apifox。作为一个集 API 文档、调试、设计、测试、Mock、自动化测试于一体的工具,Apifox 是目前提升研发效率的首选。
如果你正在开发项目,不妨试试其极其友好的界面设计,它完全兼容 Postman 和 Swagger 数据格式,导入数据非常方便,,即使是新手也能很快上手,点击这里即可注册使用。

值得一提的是,除了个人和常规团队使用,针对有高安全合规要求、或需要在内网环境协作的企业,Apifox 还提供了深度定制的私有化部署方案。
获取专属报价与部署方案
详细的私有化部署系统架构与安全白皮书
针对您公司规模的专属报价单
免费的 1v1 专属产品演示 (Demo) 机会