FlawOpen

flawopen.com/Teardowns/cve-2026-87913-aws-security-agent-mcp-s3-bucket-ownership

● CVE-2026-87913 · CVSS 5.1 · 中危
安全研究 · FlawOpen

CVE-2026-87913:AWS MCP 的 S3 数据泄露

可预测的 AWS Security Agent 存储桶曾可接收私有源码扫描归档并暴露敏感数据。升级到 0.2.0 后,还须核实存储桶账户所有权。[1], [2], [4]

💡 通俗易懂的原理解析 (ELI5)

想象快递员只确认某地址有楼,却不核实楼归谁。别人可能先登记地址并收到包裹。修复要求 S3 在检查和上传时都确认预期账户是所有者。如果包裹里有公司源码和钥匙,送错人就会泄露机密,因此还要先查清地址归属。[2], [1]

核心概念与专有名词

MCP 服务器
为 AI 助手提供工具操作的本地服务;操作使用服务器进程的权限。[3]
S3 存储桶
Amazon S3 中名称全球唯一的容器;名称本身不能证明所属 AWS 账户。[2], [3]
ExpectedBucketOwner
S3 请求参数,要求服务在操作前核验预期所有者的 AWS 账户 ID。[2], [8], [9]
CWE-283 与 CWE-341
MITRE 对所有权未验证和可从可观察信息推算的值的分类;公告映射了两项。[8], [9]

框架映射

根本原因剖析 (Root Cause)

AWS Security Agent MCP 服务器使用客户 AWS 账户 ID 和区域生成扫描输入存储桶名:security-agent-scans-<account-id>-<region>。AWS 和维护者公告指出,受影响版本只检查存储桶是否存在,没有验证其是否属于客户账户。S3 名称全球唯一,账户 ID 公开。若第三方先注册该名称,就可能收到 MCP 服务器上传的私有源码归档,其中可能有代码、凭据和基础设施状态。这是云资源所有权未验证和名称可预测问题,不是 AI 模型提示注入。受影响版本为 0.1.0 至 0.1.5;0.2.0 在 S3 操作中加入 ExpectedBucketOwner,不匹配时会失败。升级不会释放已被他人注册的名称:核实实际所有者,若属于第三方则停止扫描。

Bucket 名称不能证明其所有权

边界已记录的条件运维操作
受影响版本awslabs.security-agent-mcp-server 0.1.0 至 0.1.5 受影响;0.2.0 增加了所有权校验。升级到 0.2.0 或更高版本,并核实实际运行的软件包版本。
可预测的 Bucket 名称扫描 Bucket 名称由账户 ID 和区域生成;其他 AWS 账户可以先注册该名称。将匹配的名称视为地址,而非所有权证明。
跨账户上传易受攻击的服务器可能只按名称接受 Bucket 而不核实所有者,随后将私有扫描归档上传到该处。在扫描或上传前确认 Bucket 属于预期账户。
修复后的所有权边界ExpectedBucketOwner 让 S3 在所有权检查和上传时都比对所有者。升级并核实实际所有者;如果名称属于第三方就停止扫描。升级不会转移该 Bucket。

攻击执行流程分解

步骤 1

扫描请求处理源码

AI 客户端请求代码扫描;本地服务器打包工作区并使用已配置的 AWS 身份。[3], [2]

步骤 2

目标名称可以推算

名称由账户 ID 和区域生成;ID 并非秘密,S3 名称全球唯一。[1], [2]

步骤 3

检查只确认存储桶存在

受影响服务器按名称接受存储桶,却未确认客户账户是否拥有它。[2], [1]

步骤 4

归档可能到达其他账户

若其他账户先注册该名称,就可能收到私有扫描归档及其中的敏感信息。[2], [1]

源代码对比:漏洞与安全实现

以下为示意代码,不是逐字源码。仅确认存在并不足够:S3 必须在核验和写入时比较预期账户。预期账户应取自已认证的 AWS 身份;不匹配时立即失败。升级后仍要单独检查所有者,因为升级不会转移或释放已注册的名称。 AWS 文档说明 HeadBucket 和 PutObject 都支持 ExpectedBucketOwner:所有者不匹配时请求会以 403 失败。可将其作为 fail-closed 防护;但在认定存储桶属于他人之前,应通过有权限的所有权核验流程排查访问拒绝错误。[2], [8], [9]

源码摘录: 根据公告整理的 AWS SDK 示例;不是逐字对应的源码差异。

存在漏洞的实现
# 简化的易受攻击形式:检查了名称,但没有检查所有者。
s3.head_bucket(Bucket=bucket_name)
s3.put_object(
    Bucket=bucket_name,
    Key=object_key,
    Body=archive_bytes,
)
加固后的安全修复
# 将每项操作绑定到预期的存储桶所有者账户。
expected_owner = sts.get_caller_identity()["Account"]
s3.head_bucket(
    Bucket=bucket_name,
    ExpectedBucketOwner=expected_owner,
)
s3.put_object(
    Bucket=bucket_name,
    Key=object_key,
    Body=archive_bytes,
    ExpectedBucketOwner=expected_owner,
)

工程与系统安全加固清单

参考来源