阅读需 8 分钟

为什么会修改数据的 GET 接口需要写操作审批?

会修改数据的 GET 接口会引发重试、预览和误操作。找出隐藏的写操作,重新设计接口契约,并在发送前要求审批。

为什么会修改数据的 GET 接口需要写操作审批?

会改变状态的 GET 接口,就像穿错制服的写操作。它带来的风险并不只是理论上的。浏览器、爬虫、API 客户端、监控工具、链接预览服务、缓存和代理都会发起额外的 GET 请求,因为协议告诉它们这样做是安全的。如果你的接口会取消任务、轮换令牌、发送消息或修改记录,这些额外请求就可能变成生产环境中的实际操作。

团队通常要等到一次离奇事故后才发现这类路由,然后只修补造成问题的那个接口。这还不够。你需要找出所有外形像读取、实际会改变数据的调用,按后果为影响分类,并将它们置于与显式写操作相同的授权和审计边界之后。修改 HTTP 方法很重要,但这只是修复工作的一部分。

安全 HTTP 方法描述的是请求语义

会修改数据的 GET 接口违背了该方法背后的承诺,即使当初选择 GET 有实际原因。RFC 9110 将 GET 定义为安全方法,并说明安全请求不会要求服务器改变状态。RFC 允许日志记录和计费等附带影响,因为客户端并未请求这些影响。它并没有为那些本来就要执行修改的路由开脱。

这个区别可以揭穿一种常见的说法:「服务器读取项目时必须更新 last_seen。」如果客户端请求获取项目,而服务更新内部访问时间戳,这可能属于附带影响。如果客户端请求获取项目,而服务将发票标记为已支付、创建导出任务、消耗令牌或推进工作流,那么修改就是客户端请求的操作。把它当作写操作。

RFC 9110 还说明了为什么这件事在实际运行中值得重视。用户代理可以自动执行安全方法。浏览器可能为了生成预览而获取页面,爬虫可能跟随链接,客户端库可能在丢失响应后重试。协议语义允许这些参与者这样做。你的服务器不能指望每个调用方都读过那条没有写进文档的例外规则。

不要把安全和幂等混为一谈。DELETE 可以是幂等的,因为重复执行后资源仍然处于已删除状态,但它依然不安全,因为第一次调用改变了状态。每次请求都递增一次计数器的 GET,只有在达到阈值后停止时,才可能在狭义上具有幂等性,但它仍然不安全,因为它的目的就是改变状态。这些词回答的是不同的问题:

  • 安全性关注调用方是否请求了状态变化。
  • 幂等性关注重复发送相同请求时,预期效果是否相同。
  • 可缓存性关注中间层是否可以复用响应。

团队混淆这些术语时,常常只加上重试保护,然后以为问题已经解决。防止重复确实有帮助,但它无法阻止链接扫描器执行第一次破坏性操作。

搜索后果,而不是可疑的路由名称

要找到隐藏的修改操作,应追踪路由会导致什么,而不是相信路由名称或 HTTP 方法。名为 getReportview 的路由可能会将任务加入队列。名为 reset 的路由如果只是返回表单,也可能完全无害。请结合运行时证据和代码路径建立清单。

先列出所有注册为 GET 和 HEAD 的处理器。对于每个处理器,追踪直接写入和后续交接,包括数据库事务、发布队列消息、具有业务含义的缓存失效、邮件或聊天发送、支付、凭据变更、文件删除,以及发往其他服务的调用。GET 处理器调用内部服务时,在自己的代码库里可能看起来没有问题,但那个内部调用可能正在执行修改。继续追踪,直到能说清楚最终影响。

下面这个简单搜索可以发现大量旧代码:

rg -n 'GET|\\.get\\(|router\\.get\\(|app\\.get\\(' src
rg -n 'INSERT|UPDATE|DELETE|enqueue|publish|sendMail|charge|revoke|rotate' src

搜索结果的形状不如你据此创建的审查记录有用。为每条发现记录接口、触发条件、最终影响、受影响的系统和调用方列表。不要在影响栏里写「更新状态」。应写成「将部署 d-481 标记为已取消,并将取消操作发送给调度器」。含糊的条目会让审查人员放过严重操作。

运行时数据可以发现源代码审查遗漏的内容。在非生产环境中,使用关联 ID 发送具有代表性的请求。然后查询应用日志、任务表、外发调用日志和审计记录,查找这个 ID。如果一个 GET 请求导致消息、行变更、队列项目或外部请求,就记录完整链路。路由可能通过计划任务在几秒后才发生修改,只看请求日志会得出误导性的结论。

注意那些因为不是关系型数据库写入而被开发者忽略的修改。生成一次性下载 URL 会消耗一项能力。启动导出可能产生高额费用。读取「接受邀请」路由可能会将用户加入组织。调用报告接口可能会唤醒昂贵的数据仓库任务。你返回的资源可能是只读的,但生成它的操作并不是。

旧版 API 会把写操作藏在熟悉的位置

最糟糕的旧版路由通常起源于面向人工用户页面的快捷方式。有人让一个管理链接更容易点击,随后另一个服务复制了这个 URL,再后来某个脚本依赖它,于是这个快捷方式变成了 API 契约。

密码重置确认链接就是典型例子。像 GET /reset/confirm?token=... 这样的路由看起来很方便,因为浏览器可以直接打开。如果打开这个 URL 会消耗令牌并修改密码,邮件扫描器和预览工具就可能抢先消耗它。安全的设计是让 GET 只显示确认状态,不消耗任何东西,然后使用 POST 提交确认。页面可以携带一个短时有效的服务端引用,但只有在用户明确执行操作后才进行写入。

退订链接需要更多关注,而不是更少。邮件系统和隐私法规让一键退订很有吸引力,有些标准也确实要求这样做。如果邮箱安全扫描器跟随此类链接,收件人可能在没有打开邮件的情况下失去订阅。应在适用时使用标准请求头机制,了解接收方生态如何处理它,并明确设计接口行为。不要把通用的「GET 退订」模式复制到无关的管理 API 中,再把它当作先例。

其他常见问题包括:

  • GET /jobs/123/retry,每次仪表板刷新都会创建一次新的执行。
  • GET /deployments/123/rollback,监控探针测试链接时可能调用它。
  • GET /tokens/123/revoke,将支持链接变成破坏性能力。
  • GET /invoices/123/send,让预览机器人变成邮件发送者。
  • GET /reports/monthly,不返回已有报告,却悄悄启动高成本导出。

「只要求一个秘密查询参数」是一个错误建议。查询字符串会进入浏览器历史记录、分析系统、服务器日志、某些流程中的 Referer 请求头、截图和复制的消息。更重要的是,秘密 URL 仍然是 GET URL。任何接收到它的人或系统,都可能在没有审批边界的情况下激活该操作。

重试和预览会放大影响范围

单个带数据修改的 GET 所能触达的范围,比作者预想的更大,因为自动化调用方会把它当作可以重复执行的请求。最初的症状往往看起来毫无规律:某个操作执行了两次,账户在夜间发生变化,或用户看到自己并未执行的操作。请求日志显示使用的是合法凭据,于是事故被归为操作员错误。这个结论常常让调查过早结束。

考虑一个旧版接口,它在收到 GET /builds/77/retry 后重启远程构建。代理通过一条网络路径获取该 URL,服务器已经接受请求,但响应发生超时。代理按照许多 HTTP 客户端的常见行为进行重试。原始请求已经将构建 311 加入队列,第二次请求又将构建 312 加入队列。随后,仪表板加载活动流中的预览链接,又将构建 313 加入队列。处理器每次都可能返回 200 OK,因此响应中没有任何信息表明操作发生了重复。

重定向还会带来另一个意外。如果旧的 GET 操作重定向到新路由,而新路由仍然通过 GET 执行操作,重定向就会保留不安全的语义。如果重定向以客户端意料之外的方式改变方法,客户端的失败表现可能各不相同。重定向是迁移辅助工具,不应成为隐藏授权变化或方法语义变化的地方。

缓存会让故障变得更加难以理解。共享缓存不应在没有明确指令的情况下存储带数据修改的 GET 响应,但系统会出错,开发者也可能机械地添加缓存头。即使没有缓存存储,预取器也可能在用户决定操作之前发出请求。不要寄希望于每个中间层都能遵守你的内部意图。

修复应从边界开始。任何可能改变远程系统的操作,都需要在 HTTP 客户端发送请求前收到明确的操作请求。调用方应看到清晰的操作名称、目标和后果。这样,发送后发生超时就会被视为一次结果不确定的写操作,调用方应通过状态查询或幂等键处理,而不是盲目重放。

为操作设计写操作契约

让 API 通过 Sallyport
通过 HTTP 通道在应用内部注入 bearer、basic 或自定义请求头凭据。

修复后的接口应在 URI、方法、请求体、响应和文档中明确体现这次修改。你不需要陷入充满名词的 REST 争论,也能把它设计好。你需要的是一份能阻止调用方把操作误认为获取请求的契约。

对于构建示例,可以使用 POST 操作接口,并接受幂等键。接口应返回能够标识新执行实例的资源,而不是一条笼统的成功消息。

POST /v1/builds/77/retries HTTP/1.1
Idempotency-Key: 9ef8b462-97bf-4ca3-bb8b-4396a60ed9ae
Content-Type: application/json

{\"reason\":\"retry after failed dependency download\"}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/builds/311

{\"id\":\"311\",\"source_build\":\"77\",\"state\":\"queued\"}

将幂等键与经过身份验证的主体、操作类型、目标和请求摘要一起保存。如果同一个调用方再次发送相同的键和相同的请求,就返回原始结果。如果他们使用同一个键发送不同的请求体或目标,就返回冲突。全局共享的键记录可能让一个租户与另一个租户发生碰撞,而忽略请求体的键则可能把复制粘贴错误变成错误的操作。

当请求描述的是目标资源状态时,使用 PUT 或 PATCH。PATCH /v1/deployments/77 搭配 {\"paused\":true},在资源拥有该字段时就很合理。POST /v1/deployments/77/rollback 更能描述一个会产生新执行实例、审计记录和可能的异步结果的命令。不要为了迎合某人的风格指南,强行把命令塞进 PATCH。

返回足够的状态,让调用方能够从不确定性中恢复。如果操作是异步运行的,就返回操作 ID,并提供一个只读取进度的 GET 接口。这样,GET 可以被重试、轮询,可以根据响应头进行缓存,也可以在浏览器中打开,而不会改变系统状态。

在注入凭据之前完成审批

HTTP 请求离开设备后再审批,只是做样子。远程服务可能在客户端收到响应前就已经执行了操作,而失败响应也不能证明服务什么都没做。应在组装请求的位置做出决定,在附加凭据之前、数据离开进程之前完成审批。

当 AI 编程代理调用 API 时,这一点尤其重要。代理可能根据工具描述推断某个路由是读取操作,从代码库中复制旧 URL,或遵循工单中的建议。如果它持有原始凭据,就可能在人类看到接口之前发出调用。要求模型「小心一点」的提示,不是授权控制。

为审批层提供标准化的操作模型。至少应包含 HTTP 方法、主机、路径、可用时的目标标识符,以及简洁的后果说明。该层应根据服务契约对操作进行分类,而不是只检查 method === \"GET\"。在移除旧接口之前,调用 revokeToken 的旧版 GET 路由,必须与 POST /tokens/123/revoke 进入同一审批路径。

实际映射可以如下所示:

{
  \"method\": \"GET\",
  \"url\": \"https://api.example.test/v1/tokens/tk_42/revoke\",
  \"semantic_action\": \"revoke credential\",
  \"target\": \"tk_42\",
  \"approval\": \"required\",
  \"reason\": \"legacy GET endpoint changes remote credential state\"
}

不要只向审批人展示一个主机名和一个绿色按钮。提示应告诉他们,这次调用会撤销某项凭据,并明确即将受到影响的目标。如果系统无法确定语义操作,就将调用视为未分类并要求审批。因为是 GET 就放行所有 GET 请求,只会把原来的缺陷再复制到更深一层。

对于通过 HTTP 通道使用 Sallyport 的代理,可以在这个边界接入 Sallyport 的按会话授权和按调用凭据控制。具体应用并不影响核心设计:保险库持有者执行请求,代理只接收结果,不接触秘密。

让读取保持有用,让写入难以被误触发

查看是谁在调用
新的代理进程必须先获得会话审批,才能发起第一次外部调用。

清晰的迁移模式是保留一个用于发现的安全 GET,再为实际操作引入独立的写接口。你可以继续保留面向用户的页面、状态接口或试运行响应,同时不让获取请求执行命令。

对于报告生成器,GET /reports/monthly 可以返回最近一次完成的报告和当前生成状态。POST /reports/monthly/runs 启动一次新的生成。对于凭据操作,GET /tokens/tk_42 可以返回元数据,而 POST /tokens/tk_42/revocations 创建撤销事件。额外的路径段没有操作查询参数那么取巧,但能让日志、客户端和审查界面清晰得多。

试运行需要精确的契约。POST /deployments/77/rollback?dry_run=true 仍然是 POST,因为调用方请求的是命令评估,即使它不会提交任何修改。返回计划影响的目标、预期前置条件和任何未解析的值。如果规划过程本身会获取锁、预留容量,或联系能够观察到该操作的提供商,就不要让 GET /rollback?preview=true 执行命令规划。

有些团队尝试让旧 GET 返回一个带表单的 HTML 页面,并自动提交 POST,以保留旧集成。这只是把风险移到了浏览器中。应使用要求真实用户交互的页面,并根据应用情况为 POST 提供适当的同源防护。API 客户端应收到清晰的弃用响应和迁移期限,而不是一个它们无法使用的浏览器文档。

测试那些从不请求许可的调用方

在测试过导致它不安全的自动化行为之前,不能算真正修复了路由。只测试处理器是否调用服务方法还不够。要像浏览器、超时中的 HTTP 客户端、爬虫和代理那样访问这条路由。

对于每个已迁移的操作,在隔离环境中运行以下检查:

  1. 连续发送两次旧 GET,确认它不能创建两个操作。迁移期间,它应安全失败,只显示确认状态,或返回弃用响应。
  2. 模拟客户端在请求发送后丢失响应,然后使用相同幂等键重试 POST。确认服务返回原始操作 ID。
  3. 反复获取安全的状态 URL,确认它不会创建任务、消息、账本条目或外部调用。
  4. 使用已过期的审批或已撤销的会话尝试操作,确认请求不会到达远程服务。
  5. 检查审计记录,确认其中记录的是标准化操作,而不只是传输路由。

在最容易出问题的时间点注入故障:服务器提交操作之后、发送响应之前。团队会在这里发现客户端是否会盲目重试。如果唯一的恢复策略是「再试一次」,说明契约没有为调用方提供足够信息。

还要测试文档中的示例。复制到事故频道里的 curl 命令会变成操作接口。如果示例为了省一行而使用 GET,就有人会把它自动化。应让安全的读取示例和明确的操作示例在视觉上明显不同。

同时审计影响和路由

在保险库处拦截操作
锁定保险库后,Sallyport 会拒绝所有操作,直到保险库再次打开。

写着 GET /v1/builds/77/retry 200 的审计记录证据价值很低。它只记录了传输事实,却隐藏了业务事件。事故期间,调查人员仍然需要重建这次调用究竟启动了构建、重试了早先的构建,还是仅仅返回了状态。

应同时记录这两个层面。保留收到的方法和路由,因为旧行为仍然重要。同时记录语义操作、目标、调用方进程或主体、审批决定、不含秘密材料的凭据身份、关联 ID 和结果引用。对于异步命令,记录操作 ID 或生成的资源 ID,让后续事件可以关联到原始请求。

只有在信任已经受到质疑后仍能完成验证,防篡改日志才真正有用。验证过程应与应用的正常读取路径分离。Sallyport 将会话日志和调用日志写入防止应用读取的加密哈希链式审计日志中,sp audit verify 可以离线对密文验证这条链。这正是当代理有权影响外部系统时,值得要求的能力。

不要让审计保留期限成为记录秘密的借口。请求体经常包含凭据、令牌、个人数据或不应进入一般活动日志的命令参数。在需要完整性证据时,记录标准化描述和摘要。敏感材料只应存储在访问控制和保留规则能够支持它的位置。

删除例外,而不是永远记录它

最终状态不应存在任何会修改数据的 GET 路由,即使当前有审批网关能够拦截它们。保留这个例外,会给新客户端、复制的 URL 或未来的重构留下绕过分类表的机会。兼容层应有明确负责人、调用方清单,以及停止接受旧形式的日期。

先处理可能造成最不可逆后果的接口。增加明确的 POST 契约、幂等行为、审批分类和影响级别的审计记录。然后让每一次旧 GET 调用都可观测。当你能说清楚剩余调用方是谁时,就可以有计划地迁移,而不是意外破坏某个隐藏集成。

不要把「我们的客户端更懂」当成安全属性。GET 请求会经过那些专门设计来重复和检查请求的系统。在其中一个系统决定自作主张之前,先让你的写操作看起来像真正的写操作。

常见问题

GET 请求可以合法地修改数据吗?

不能。GET 被定义为安全的 HTTP 方法,因为请求的语义不应改变服务器状态。服务器仍然可以记录请求,或在内部更新缓存,但如果 GET 会删除、重启、发送、计费或修改记录,就破坏了调用方所依赖的契约。

如何找到带副作用的 GET 接口?

先从名称中包含 retry、cancel、reset、resend、rotate、sync、export、confirm 或 preview 的路由开始。然后将请求日志与这些路由运行后立即发生的数据库写入、任务入队、外发消息和第三方调用进行比对。

发送邮件的 GET 接口应该要求审批吗?

如果请求的结果会改变账户、资源、工作流、权限、计费状态、外部系统或消息发送,就应把它视为写操作。HTTP 方法只能说明调用意图,不能证明操作无害。

带数据修改的 GET 请求为什么在重试场景下很危险?

请求可能在超时后被重试,也可能跟随重定向、预取链接、刷新页面,或在生成预览时获取资源。用户可能以为自己只点击了一次,但服务器实际收到了两次或更多请求。

身份验证足以保护会改变状态的 GET 请求吗?

不能。身份验证回答的是谁可以调用接口,审批回答的是现在是否应该执行这次具体操作。代理携带长期凭据,并不能让意外的状态变化变得可以接受。

旧版 GET 操作应该改用 POST、PUT 还是 PATCH?

当操作触发命令时,例如 /jobs/{id}/cancel,使用 POST 操作接口,并记录其结果。当调用方提供资源的完整替代表示或部分表示时,使用 PUT 或 PATCH。

为了向后兼容,可以保留旧的 GET 路由吗?

只有在能够统计并迁移调用方时,才临时保留旧路由。可以让它只重定向到不会修改数据的确认页面,拒绝不安全的自动化调用,或返回清晰的弃用响应,同时让客户端迁移到新的操作接口。

幂等键能让不安全的 GET 接口变得安全吗?

可以,前提是每次尝试都携带幂等键,并且服务在适当的时间内保存第一次完成的结果。这样能防止重复传递,但不能让带数据修改的 GET 对爬虫或预览服务变得安全。

带数据修改的 API 请求应该记录哪些审计信息?

记录标准化后的操作、目标、调用方身份、授权决定、结果和关联 ID。同时记录传入的 HTTP 方法和路由,这样调查人员才能证明某个旧的读取形状请求实际执行了写操作。

AI 代理如何在看不到凭据的情况下审批危险的 API 调用?

把审批边界放在客户端发送 HTTP 请求之前,因为远程服务可能在响应返回前就已经执行了操作。代理通过 Sallyport 的 HTTP 通道使用服务时,Sallyport 可以完成这件事:代理请求执行某项操作,由应用注入凭据并记录调用。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov