# HTTP 方法覆盖会击穿只看方法的审核

当审核者只看请求行时，按方法审核并不安全。请求可以用 `POST` 到达，携带 `X-HTTP-Method-Override: DELETE`，随后由应用中间件改写方法并进入删除处理器。如果审批界面、网关规则、授权检查或审计记录把它归类为普通 POST，它审核的只是外壳，漏掉了真正的操作。

这种行为不是所有服务器都理解的 HTTP 协议技巧。它是一种应用约定，因此很容易被忽略。是否支持它取决于框架、中间件顺序、路由和部署。正确做法不是假定每个 POST 都暗藏破坏性，而是找出哪些位置接受覆盖，在任何安全判断前解析覆盖，并拒绝有歧义的请求。

## 线上方法和实际方法是两个不同事实

支持覆盖的技术栈需要记录两个方法：HTTP 请求行中的方法，以及应用最终用于路由的方法。我把它们称为线上方法和实际方法。它们通常一致。危险发生在中间层按线上方法审批或过滤，而后续组件按实际方法路由时。

RFC 9110 规定，方法令牌是请求语义的主要来源。它把 POST 定义为针对资源的处理，把 DELETE 定义为要求源服务器移除目标资源与当前功能之间的关联。RFC 并未标准化 `X-HTTP-Method-Override`。这个标头属于一组兼容性约定，当年用于只能处理 GET 和 POST 的客户端或中间层。

普通软件至今仍实现这些约定。Express 的 `method-override` 中间件默认读取 POST 上的 `X-HTTP-Method-Override`。它接受值后会修改 `req.method`，并把旧值保存在 `req.originalMethod`。ASP.NET Core 也提供 HTTP 方法覆盖中间件，默认标头同样是 `X-HTTP-Method-Override`。Spring 的 `HiddenHttpMethodFilter` 走另一条路：它读取 POST 表单中的 `_method` 参数，并允许 PUT、DELETE 和 PATCH。

这些细节把团队经常混为一谈的概念分开了。“网关允许 POST”描述传输处理，“应用执行 DELETE”描述实际行为。审批或访问决定需要后者。如果做决定的组件无法确定实际方法，就必须拒绝覆盖信号，不能按外层动词静默分类。

框架支持本身不能证明系统暴露了问题。Express 应用必须安装中间件并正确排序，ASP.NET Core 应用必须添加中间件，Spring 应用必须启用并放置过滤器。最终结果由实际部署配置决定，其中还包括反向代理和按路由配置的中间件。检查源码可以找到候选点，感知状态的测试才能提供证据。

## 获批的 POST 可以进入 DELETE 处理器

当不同组件对操作的权威表示意见不一时，隧道化写入就会溜过去。设想代理要向项目 API 发出以下请求：

```http
POST /v1/projects/42 HTTP/1.1
Host: api.example.test
Authorization: Bearer [injected outside the agent]
X-HTTP-Method-Override: DELETE
Content-Length: 0
```

审批层把它标为“POST /v1/projects/42”，并套用允许本会话使用 POST 的规则。反向代理转发陌生标头。RFC 9110 通常要求代理转发无法识别的字段，除非配置明确阻止或转换它们。在应用内部，覆盖中间件先改写方法，路由器随后选择 DELETE 处理器，项目就消失了。

这种故障不需要有缺陷的 DELETE 授权检查。应用可能正确确认该身份有删除权限。缺陷出现在更早处：人工或自动审核器忽略了一个受支持的输入，因此批准了实质上不同的操作。同样的分歧还会影响 Web 应用防火墙、限速器、CSRF 控制、请求指标和访问日志。

中间件顺序决定哪些控制能看到哪个事实。Express 文档对此说得很直接：方法覆盖必须放在所有需要了解请求方法的中间件之前。对单个进程内的路由和 CSRF 逻辑，这个建议是对的。但它无法修复进程外的网关，因为网关早已根据请求行做出了基于方法的决定。

还有一种更隐蔽的故障。边缘日志记 POST，应用日志记 DELETE，事件调查者却把它们当成无关请求。共享请求标识符可以关联记录，但前提是两端都保留它，而且团队知道需要比较方法字段。统一的规范化操作记录更不容易出错。

不要仅凭 DELETE 这个词推断影响。RFC 9110 指出，DELETE 移除的是资源与其当前功能之间的关联；是否回收存储、是否销毁内容由应用决定。DELETE 端点可能执行归档、停用、排队或删除数据。审核实际路由后果，不要只看通用动词定义。

## 用受控的方法矩阵测试端点

可靠测试要在一次性资源上，对比原生方法、覆盖标头和参数约定造成的状态结果。请在预发布环境中运行，或使用专为破坏性测试准备的夹具。测试身份只应拥有真实调用者的权限，而且每个用例前都要重新创建夹具，避免一次成功删除污染后续结果。

先发送不含覆盖的基线 POST，记录状态码、响应正文和夹具状态。然后发送原生 DELETE，以确认路由确实存在，而且测试身份可以执行它。最后对每种约定重复 POST。最小标头探测如下：

```sh
BASE='https://staging.example.test'
ID='override-probe-17'

curl -sS \n  -D response.headers \n  -o response.body \n  -w 'case=x-http-method-override outer=POST status=%{http_code} bytes=%{size_download}
' \n  -X POST "$BASE/v1/projects/$ID" \n  -H 'Authorization: Bearer test-token' \n  -H 'X-HTTP-Method-Override: DELETE'

curl -sS \n  -o state.body \n  -w 'verify=read-after-request status=%{http_code} bytes=%{size_download}
' \n  -H 'Authorization: Bearer test-token' \n  "$BASE/v1/projects/$ID"
```

输出刻意采用便于 CI 解析的稳定格式：

```text
case=x-http-method-override outer=POST status=204 bytes=0
verify=read-after-request status=404 bytes=71
```

这个示例表示覆盖确实删除了夹具，并不代表所有系统都应返回同一状态。有些 API 返回带结果文档的 200，排队删除时返回 202，也有系统保留仍可读取的软删除表示。应按自己应用的契约定义成功。

测试要用矩阵，而不是随手拼接步骤。控制组是不含覆盖的 POST，必须产生普通 POST 行为。原生组是不含覆盖的 DELETE，用来确定文档所述的删除行为。随后分别发送带 `X-HTTP-Method-Override: DELETE`、`X-HTTP-Method: DELETE` 和 `X-Method-Override: DELETE` 的 POST。最后测试查询中的 `?_method=DELETE` 以及表单正文中的 `_method=DELETE`。每个覆盖用例都必须符合你明确设计的隧道方法行为，否则就应失败且不改变状态。

OWASP Web Security Testing Guide 列出了上述三个标头变体，并建议在受限方法被拒绝时用覆盖标头重放请求。我认为只比较状态码还不够，因为差异可能来自验证、路由或中间层。每次探测后都要检查资源、资源版本和任何排队任务。

如果你控制整个技术栈，就在每一跳抓取观察结果：边缘请求日志、审批事件、应用请求日志、所选路由和数据结果。目标是找出不一致。安全结果可以是一致拒绝，也可以是有意规范化后，对它执行与原生 DELETE 相同的授权和审批。

## 状态码只是线索，状态变化才是证据

单凭 HTTP 状态码无法判断覆盖是否执行。204 之后夹具消失，是强证据。200 可能是普通 POST 响应、删除回执，也可能是错误地用成功码包装的自定义错误。405 可能由覆盖中间件之前的边缘层返回，而另一条路径或内容类型仍能到达中间件。

断言应围绕带唯一标识符和已知初始版本的金丝雀资源建立。探测前先获取资源，保留版本标记或表示摘要。探测后再次读取，并检查 API 返回的操作记录。测试更新时，选择无害且一眼可辨的字段；测试删除时，先弄清产品采用硬删除、软删除还是延迟清理。

响应比较仍有帮助。保存状态码、选定响应标头、正文大小和正文摘要。把覆盖请求同时与两个控制组比较：普通 POST 和原生 DELETE。如果覆盖响应像 DELETE，状态也与 DELETE 一致，结论就很有说服力。如果响应像 POST，状态却按 DELETE 改变，日志层或响应层可能仍在使用外层方法。

重定向需要单独检查。客户端在跟随 301、302、303、307 或 308 时可能改变请求行为，不同工具保留方法和正文的规则也不同。先禁用重定向运行并记录 `Location` 字段，再有意跟随并捕获每一跳。不要把重定向行为和覆盖行为混进一个看不透的结果。

异步 API 需要更长观察窗口，但这不是猜测的理由。如果响应提供操作标识符，就按文档规定的测试契约轮询。如果没有持久句柄，就在预发布环境中查看队列或应用日志。无法确定的用例应标为无法确定，而不是安全。

还要验证负面状态。被拒绝的覆盖不应创建、更新、归档、排队、发送邮件或触发高权限 Webhook。整齐的 4xx 很容易误导团队：后面的组件可能已提交副作用，前面的组件随后才拒绝响应路径。测试夹具的监控必须足以发现这种顺序。

## 标头变体和歧义应放在同一测试中

只测标准拼写会漏掉兼容代码和解析器分歧。常见标头名是 `X-HTTP-Method-Override`、`X-HTTP-Method` 和 `X-Method-Override`，但应用也能定义自有读取器。查询字符串和表单正文可携带 `_method`，旧集成有时还使用与厂商或框架相关的名称。

标头名大小写不是另一种约定。RFC 9110 规定字段名不区分大小写，因此 `x-http-method-override` 和 `X-HTTP-Method-Override` 指同一字段。只匹配首字母大写写法的安全过滤器有缺陷，即使应用库会规范化标头名。

重复和冲突值暴露了更难处理的问题。字段定义允许时，RFC 9110 允许接收方把重复字段行合并为逗号分隔值，而且提醒实现者，即便字段预期只有一个值也要考虑重复情况。覆盖标头不是具有统一解析规则的标准单值字段。Express 中间件文档称它使用重复标头的第一个值，而安装多个覆盖处理器还会在不同标头名之间形成优先级。

基本矩阵之后加入大小写变体 `x-http-method-override: DELETE`，它必须和标准大小写结果一致。发送两个相同 DELETE 值，要求得到一种有文档说明的结果或明确拒绝。先发 PUT 后发 DELETE，再在两个覆盖标头名中放入不同值，这两类冲突都应因歧义被拒绝。还要拒绝 `PUT, DELETE` 这种逗号值、原生 PATCH 配覆盖 DELETE（除非契约明确规定），以及 DELETE 标头配 `_method=PATCH`。

不要只停在 DELETE。还要探测 PUT 和 PATCH，因为审核系统可能对创建、替换、局部更新和删除有不同分类。用一个不支持的令牌作为负面对照。除非测试环境和范围明确允许，否则不要测试 TRACE 和 CONNECT；它们会触发不同的基础设施行为，却不会让覆盖结论更可靠。

空白和取值大小写也能暴露规范化不一致。在客户端允许时发送 `delete`、` DELETE ` 和空值。安全规则很简单：只规范化文档约定允许的形式，按明确方法集合验证，其余全部拒绝。悄悄选择第一个可解析值，只会在代理或库变化后让绕过再次出现。

## 代理生成的请求更容易触发盲点

AI 代理不会创造方法覆盖弱点，但会让不完整审核变得更危险。代理可以任意组合标头、复用文档示例，还可能在原生 DELETE 失败后把它改成 POST 隧道重试，却不理解第二种形式跨过了审批边界。查看精简审批卡片的人很可能只盯着显眼的动词和路径。

把代理生成的每个请求组成部分都视为不可信操作输入，其中也包括看似兼容性元数据的标头。在模型外注入凭据可以减少秘密暴露，却不能让请求的操作变得无害。请求仍会把代理选择的路径、方法、标头、查询和正文交给一个可能联合解释它们的系统。

工具模式能在请求产生前减少歧义。允许自由填写标头的通用 HTTP 工具，给了调用者请求覆盖所需的全部词汇。面向具体路由的删除工具可以直接说明破坏性操作，并省去覆盖字段。第二种设计更容易审核，但前提是实现没有通过隐藏出口继续传入额外标头。

如果必须使用通用 HTTP 工具，就在展示给人工审批前解析拟议请求。解析器应与执行器使用同一套覆盖清单和优先级规则。审批后冻结规范化操作，防止执行器稍后解析出不同结果。如果任何中间层会在审批后更改标头，就把确定性转换纳入被审核操作，或者重新解析并要求再次审批。

重试同样需要关注。POST 天生不安全，也不保证幂等。RFC 9110 对 POST 的定义很宽，即针对资源的处理；许多 POST 端点无需任何覆盖就会创建或修改数据。代理工具不能因为外层方法是 POST 就判断可以安全重试，也不能把所有 POST 都升级为破坏性审批。它需要结合路由语义和实际方法。

评估代理行为时，要分开测试身份和生产身份。权限过宽的生产凭据会让每个探测都成功，从而掩盖授权差异。使用代理实际会持有的受限角色，确认原生与隧道形式得到相同决定，并记录人工审批实际展示了什么。只有把拟议操作、展示操作、执行操作和结果状态串成一条链，才能证明这是审核不一致。

## 最早的控制必须识别实际操作

所有基于方法的决定都必须放在一个统一解析器之后，否则就要拒绝全部覆盖输入。人工审批、授权、CSRF 防护、路由限制、限速和审计分类都适用。让每个控制各自重复临时解析，必然产生分歧。

要明确表示解析结果：

```json
{
  "transport_method": "POST",
  "effective_method": "DELETE",
  "override_source": "header:x-http-method-override",
  "override_value": "DELETE",
  "target": "/v1/projects/42",
  "ambiguous": false
}
```

解析器应列出所有受支持标头和参数，收集提供的全部值，只要出现一个以上不同候选值就拒绝请求。它通常只应接受来自 POST 等允许外层方法的覆盖。它要按应用有意支持的方法验证取值，再为下游控制生成不可变的操作记录。

把授权放在解析之后，并绑定路由与实际方法。“这个身份能否向此路径 POST”在 POST 充当隧道时太弱。应问“这个身份在当前条件下能否对该资源执行 DELETE”。无论客户端发原生 DELETE 还是受支持的隧道形式，都应使用同一处理器授权。

审批界面应先显示后果，并在两种表示不同时同时展示。`DELETE /v1/projects/42 via POST override` 这种紧凑标签既给出实际操作，也保留传输细节。如果只把覆盖标头藏在可展开的原始请求面板里，就等于要求审核者在时间压力下自己找出危险事实。

如果上游网关无法复现应用的解析规则，不要只教它理解其中一小部分。要么在审批前拒绝任何已知覆盖信号，要么把审批点移到可信的规范化之后。网关理解一个标头而应用理解三个，只会制造虚假的覆盖感。

解析要和业务语义分开。解析器可以得出实际方法是 DELETE，路由则负责决定它代表归档、撤销、解绑还是擦除。高影响操作的审批系统需要了解路由的操作描述。规范化方法是必要条件，但一个动词无法描述所有效果。

## 只剥离一个标头并不能修好问题

只有在服务不打算支持任何覆盖路径时，在代理处移除 `X-HTTP-Method-Override` 才安全。这个做法很受欢迎，因为简单，而且常能阻止第一个概念验证。但如果应用还接受别的标头、`_method` 参数、直达上游的路由或后续中间件，它就会失败。

Microsoft 的 YARP 标头指南列出 `X-HTTP-Method-Override`、`X-HTTP-Method` 和 `X-Method-Override`，说明代理默认会转发它们，并建议在运营方需要防止绕过时使用请求标头移除转换。这是实用的边缘加固，却不能证明目标应用忽略查询或正文覆盖，也不覆盖绕过 YARP 直达目标的流量。

有两种可靠设计可选。如果目前没有客户端需要兼容隧道，就在应用中禁用中间件，并在边缘拒绝全部已知信号。如果客户端确实依赖隧道，就只记录一种接受的输入，移除或拒绝其他形式，在所有控制之前规范化，并把原生与隧道请求作为同一个操作测试。

选择前先盘点代码和配置。搜索框架中间件、自定义请求包装器、`_method`、三个常见标头名，以及任何给请求方法赋值的代码。检查路由组，因为中间件可能只应用于某个 API 前缀。还要检查框架升级后保留下来的生成式 API 网关和旧兼容模块。

接着检查网络路径。内容分发层、负载均衡器、服务网格、反向代理、应用服务器和框架，可能用不同方式规范化或记录标头。确认没有公开路由能绕过执行拒绝的组件。对内部代理流量，也不要因为网络是私有的就信任调用者可控标头。

剥离可能破坏合法客户端，所以团队常常推迟。移除前先记录覆盖信号是否出现，但不要记录秘密或完整正文。宣布弃用，在测试环境明确失败，并同时移除服务器支持和边缘规则。让无文档兼容行为永久存在，成本往往高于迁移仍在使用它的客户端。

## 日志必须同时记录请求行和所选操作

审计记录应保留传输方法、实际方法、覆盖来源、规范化值、所选路由、认证身份、目标资源、决定、响应状态和确认结果。缺少两个方法中的任何一个，审核者都无法重建 POST 为何进入 DELETE 处理器；缺少结果，记录只能说明意图，不能说明效果。

有歧义而被拒绝的请求也要记录。冲突覆盖值即使从未进入处理器，也与安全相关。只存标头名和规范化方法令牌，不要存授权值或无关请求正文。遇到无效值时，保存长度受限且经过转义的表示，防止控制字符伪造日志行。

用第一个可信节点生成的请求标识符关联各层记录。不要直接接受外部标识符，除非先替换或加命名空间。边缘层、解析器、审批服务、应用和异步工作进程都应传递可信标识符，让调查者无需依赖时间戳就能追踪一个操作。

方法字段也要准确命名。通用的 `method` 属性会诱使各组件写入不同含义。用 `transport_method` 表示请求行，用 `effective_method` 表示路由操作。如果中间件暴露 `originalMethod`，应明确映射，不要假定这个名称在所有框架里含义相同。

Sallyport 执行代理的 HTTP 调用，不向代理暴露凭据，并在 Activity 日志中记录调用。这种隔离不会让覆盖变安全，审核仍须在执行前识别实际方法。

自动运行结束后，防篡改证据很重要。只能追加的哈希链记录可以暴露静默修改，但完整性无法补回缺失语义。应在做决定时记录规范化操作，不要让后续任务尝试从访问日志推断。

审核面板应直接显示不一致。按服务和覆盖来源统计 `transport_method != effective_method`，能很快发现意外兼容流量。新出现的来源、有歧义的请求，以及审批事件只记录外层方法的破坏性实际方法，都应触发告警。

## 回归测试必须以拒绝为默认

持久修复需要一项贯穿实际部署路径的契约测试，只要未获批准的覆盖改变状态就失败。解析器单元测试有帮助，却无法发现代理规则消失、中间件被移到授权之前，或某条路由通过另一入口变得可达。

为矩阵每一行写明预期行为。如果禁用隧道，每个覆盖信号都应返回有文档说明的 4xx，而且夹具不变。如果支持一种约定，其 DELETE 结果应在授权、审批分类、路由和审计字段上与原生 DELETE 一致。冲突和未知令牌必须在产生副作用前失败。

部署路径测试应包括以下断言：

- 审批事件明确写出实际方法；
- 原生和隧道形式得到相同授权决定；
- 被拒绝输入不会改变金丝雀版本或副作用计数；
- 日志在同一可信请求标识符下包含两个方法字段；
- 直达上游的访问无法跳过规范化。

当代理配置、框架中间件、认证、审批代码、路由或代理工具变化时运行测试套件。每夜运行可发现基础设施漂移，发布门禁可发现有意变更。夹具要保持隔离，确保重复执行永不触碰真实人员数据。

即使框架称其为兼容功能，新接受一种约定也应按安全变更处理。它需要明确负责人、客户端需求、优先级规则、允许的外层方法、允许的实际方法集合以及移除条件。否则，一行中间件配置就会悄悄扩展控制必须理解的操作词汇。

干净的系统从入口到结果只使用一份操作描述。如果线上写 POST，应用里执行 DELETE，这个差异必须在任何人或组件说“同意”前明确出现。再晚一步，就只能做事件重建。
