Skip to content

[Bug]: 未校验 callback 可信 host 仍创建异步 Task #100

Description

Checklist

  • 我已经搜索过相关问题,但没有得到预期的帮助。
  • 最新版本中该错误尚未修复。
  • 请注意,如果您提交的Bug描述缺少相应的环境信息和最小可复现的demo,我们将很难复现和解决该问题,从而降低收到反馈的可能性,甚至该问题将被关闭。

🐞 问题详细描述

1. 问题概述

ReactAgent webhook 测试发现:在 callback 能力已经启用时,调用方可以通过SendMessage.params.pushNotificationConfig.callbackUrl 提交任意合法格式的 HTTP/HTTPS URL。即使目标 host不在受信任 runtime、registry、allowlist 或部署配置中,runtime 仍返回 JSON-RPC result,而不是在 Task 创建前返回trust-policy error 或 -32602 Invalid params

本次使用的 callback URL 为:

http://untrusted.invalid/a2a/push-notifications/callback

PR 151 最新提交已经解决原问题 :当 pushNotifications=false 时,inline push config 会返回-32003 Push Notification is not supported。测试继续执行后,在“不可信 callback target”子场景失败,说明能力关闭门控已生效,但 callback target 的 trusted-host 校验仍未实现。

代码核查确认,当前 A2aPushNotificationCallbackUrlPolicy 只校验 URL 是否为绝对 URI、是否存在 host,以及 scheme 是否为HTTP/HTTPS;没有读取或匹配 trusted-host/allowlist 配置。现有模块测试甚至明确把https://evil.example/... 视为可进入 SDK handler 的合法 URL。

2. 当前 runtime 代码实现

2.1 PR 151 已增加能力关闭门控

文件:

service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/A2aJsonRpcController.java

当前代码在 inline push config 存在时先检查部署开关:

if (!a2aProperties.isPushNotifications()) {
    throw new PushNotificationNotSupportedError();
}
A2aPushNotificationCallbackUrlPolicy.validateCallbackUrl(
        params.configuration().taskPushNotificationConfig());

该修改解决了原问题,但后续 URL policy 没有可信 host 输入,因此不能解决本问题。

2.2 URL policy 只校验 URL 格式

文件:

service/agent-service-app/src/main/java/com/openjiuwen/service/app/controller/a2a/A2aPushNotificationCallbackUrlPolicy.java

当前实现:

URI uri = new URI(callbackUrl);
String scheme = uri.getScheme();
if (!uri.isAbsolute() || scheme == null || uri.getHost() == null || uri.getHost().isBlank()
        || (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme))) {
    return Optional.empty();
}
return Optional.of(uri);

这只能拒绝空值、相对 URL、无 host URL 和非 HTTP/HTTPS scheme。untrusted.invalidevil.example 都是语法合法的HTTP host,因此会被放行。

2.3 当前没有 trusted-host/allowlist 实现

在 PR 151 的 service 模块中搜索 trustedHosttrusted-hosttrusted hostsallowlist,没有找到对应生产实现或配置项。

当前 A2AProperties、controller 和 URL policy 之间也没有传递可信 host 集合,因此系统无法区分:

  • 受信任的 runtime callback endpoint;
  • 普通 client 自报的任意公网 URL;
  • 内网地址、回环地址或其他可能引发 SSRF 的目标。

2.4 现有模块测试与设计要求相反

文件:

service/agent-service-app/src/test/java/com/openjiuwen/service/app/controller/a2a/A2aJsonRpcControllerTest.java

测试 absoluteHttpInlinePushConfigReachesSdkHandler() 使用:

https://evil.example/a2a/push-notifications/callback

并断言:

verify(requestHandler).onMessageSend(...);

这实际上把“任意绝对 HTTP/HTTPS URL”作为可接受行为固定了下来,与 FEAT-001/L2 的 trusted-host 要求冲突。该测试需要改为:

  • allowlist 内 host 可以进入 handler;
  • allowlist 外 host 必须返回错误且 verifyNoInteractions(requestHandler)

2.5 sender 复用同一弱 URL policy

HttpPushNotificationSender 同样调用 A2aPushNotificationCallbackUrlPolicy.callbackUri()。因此即使将 ingress 拒绝遗漏,outbound sender 也没有第二道 trusted-host 防线,存在向任意 HTTP/HTTPS host 投递 Task 结果的风险。

3. 当前实现与设计的冲突

对比项 文档预期 当前实现/测试表现
callback URL 语法 必须合法 已校验 absolute URI、host、HTTP/HTTPS
callback host 信任 必须来自 trusted runtime/registry/allowlist/部署配置 未实现,任意合法 HTTP/HTTPS host 被接受
ingress 拒绝时机 保存配置和创建 Task 前 不可信 URL 进入 SDK handler
Task/config 副作用 不可信时不得创建异步执行、不得保存配置 response 不含 error,未在 ingress 拦截
outbound 防线 不得向不可信 URL 投递 sender 复用只校验格式的 policy
Agent Card capability 依赖 trusted hosts 可用性 当前没有 trusted-host 配置参与能力计算
模块测试 覆盖 allowlist 内允许、allowlist 外拒绝 当前断言 evil.example 应进入 handler

4. 建议修改方向

4.0 修改项关系说明

修改项 是否必须 关系说明
4.1 定义 trusted-host 配置与匹配规则 必须 没有可信来源就无法执行信任判定
4.2 ingress 在 Task 创建前校验 必须 直接满足 L2 的无副作用拒绝要求
4.3 sender 投递前再次校验 必须 防止存量配置、旁路写入或竞态绕过 ingress
4.4 capability 绑定 trusted-host 可用性 必须 确保 Agent Card 不夸大能力
4.5 SSRF 安全规则 必须明确 需要覆盖 DNS、IP、端口和重定向,而不只是字符串后缀
4.6 补充测试 必须 验证允许和拒绝路径均无回归

推荐整体落地:统一 trusted-target policy + ingress 前置校验 + sender 二次校验 + capability gate 接入 + 完整模块/黑盒测试

4.1 定义可信 callback target 配置

建议由 registry 或部署配置提供受信任 runtime endpoint。若首迭代采用静态配置,应至少明确:

  • 允许的 scheme;
  • 精确 host 或 CIDR,不使用宽泛字符串后缀匹配;
  • 允许的端口;
  • 固定 callback path;
  • 是否允许回环、私网和 link-local 地址;
  • DNS 解析及重绑定防护;
  • HTTP redirect 是否禁止或每一跳重新校验。

示意配置名可由开发按现有规范确定,例如:

openjiuwen:
  service:
    a2a:
      push-notifications: true
      trusted-callback-targets:
        - scheme: http
          host: 127.0.0.1
          port: 18080
          path: /a2a/push-notifications/callback

该名称只是建议,不应在未确认配置规范前作为最终契约。

4.2 ingress 在 Task 创建前校验

建议 controller 调用一个可注入配置的统一 policy:

private void validateInlinePushNotificationConfig(MessageSendParams params) {
    TaskPushNotificationConfig config = inlinePushConfig(params);
    if (config == null) {
        return;
    }
    if (!a2aProperties.isPushNotifications()) {
        throw new PushNotificationNotSupportedError();
    }
    trustedCallbackTargetPolicy.requireTrusted(config.url());
}

requireTrusted() 应同时完成 URL 语法和可信 endpoint 校验。校验失败必须抛出可映射为 trust-policy error 或 -32602A2AError,并确保 requestHandler.onMessageSend() 未被调用。

4.3 sender 投递前二次校验

HttpPushNotificationSender 应在每次 HTTP 请求前复用同一个 trusted-target policy,而不能只依赖 ingress。这样可以防止:

  • 历史或外部写入的非法 config;
  • Task/config store 被其他路径绕过;
  • DNS 或 registry 状态变化;
  • redirect 将请求导向不可信地址。

校验失败应记录可观察的 delivery failure,不得向目标发包,也不得改变 Task 终态。

4.4 capability 绑定 trusted-host 可用性

只有在 trusted target 来源已经配置并可用时,Agent Card 才能声明 pushNotifications=true。否则应保持 false,避免出现能力已声明但任何合法 callback endpoint 都无法通过,或未配置 allowlist 却允许任意 URL 的状态。

4.5 补充模块和黑盒测试

建议至少覆盖:

  • callback 能力关闭:返回 -32003 且 handler 无交互;
  • callback 能力开启 + allowlist 内精确 endpoint:允许进入 handler;
  • callback 能力开启 + allowlist 外 HTTP/HTTPS host:返回 trust-policy error/-32602,handler 无交互;
  • scheme、host 或 port 不匹配:拒绝;
  • path 不匹配:若采用固定 endpoint 契约则拒绝;
  • DNS 解析到 loopback/private/link-local:按明确策略拒绝;
  • HTTP redirect 到不可信 host:禁止跟随或重新校验后拒绝;
  • sender 收到旁路写入的不可信 config:不发起 HTTP 请求;
  • 拒绝时 config store 无新增记录、TaskStore 无新增 Task;
  • Agent Card capability 与 trusted-host 配置完整性一致。

现有 absoluteHttpInlinePushConfigReachesSdkHandler() 应改为可信 endpoint 测试;evil.example 应进入拒绝测试。

详细的环境信息描述

agent-runtime-java PR 151: d127595
提交说明: fix(a2a): satisfy code check exception boundary

其他辅助信息

版本信息

感谢您的贡献 🎉!

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions