fix(mcp): #459 身份透传功能 3 轮对抗式审查(修复 22 项发现) - #540
Open
ncw1992120 wants to merge 3 commits into
Open
Conversation
Addresses 8 confirmed findings from the first adversarial review pass on the merged mateaix#460/mateaix#471/mateaix#464 implementation: - signingKey rotation: a cached key was served forever once parsed, so a valid→valid PEM rotation never took effect without a restart (contradicted the self-heal Javadoc; dangerous when retiring a suspected-compromised key). Now tracks lastSuccessfulPem and re-parses on PEM change. - requesterUserId drop: the approval-replay and queued-message web-origin sites used overloads that lost the immutable user id, so on-behalf-of sub fell back to the mutable username typed 'authenticated'. Plumbed requesterUserIdOf(auth) into both origins. - doc drift (en+zh): plaintext value is '<trust>:<subject>' (not 'username'); JWT carries trust + channel_type claims; sub is the immutable numeric id; added an Identity-typing section + webchat/IM caveat; fixed Python examples. - progress-path identity injection (ProgressAwareMcpToolCallback bypasses the normal call()) now has behavioral tests for plaintext/token/no-identity. - NONE-branch coverage: web+no-user+no-ThreadLocal, non-web+blank-requester. - fast-path reuse + valid→valid rotation + 16-thread DCL stress tests. - audienceFor(null,null) now rejects instead of returning a colliding 'null'. The new concurrency test caught a regression in the first rotation draft (fast-path null short-circuit returned empty mid-parse under contention); fixed by moving all null-returning decisions inside the synchronized block. Tests: 216 run, 0 failures (identity-forward subset 23→41).
Addresses 14 confirmed findings from the second adversarial pass (robustness + test-quality + integration lenses) on the post-R1 code: signingKey() state machine (R2-1 HIGH, R2-2 MEDIUM): - valid→invalid PEM rotation re-parsed + re-logged the ERROR on every call while silently keeping the stale old key (CPU/log spam, no operator signal). - null/empty PEM logged the ERROR on every call (dedup couldn't match null). Both fixed: dedup now covers null-key AND stale-key cases via Objects.equals(lastAttemptedPem, pem) + an attemptedPem flag distinguishing first-ever-call from already-tried-null. The bad/empty PEM is parsed/logged exactly once; a prior good key keeps serving (fail-soft). Javadoc accuracy (I-1 MEDIUM): - signingKey() claimed self-heal on 'config reload / no restart', but the properties bean is plain @ConfigurationProperties (not @RefreshScope, no spring-cloud) so a static application.yml edit does NOT take effect without a restart. Rewrote the Javadoc to state the truth. Test hardening (TQ-1/3/4/5 HIGH, TQ-6/7/8 MEDIUM, TQ-9/10/13 LOW, R2-3 LOW): - concurrency test now asserts the key is parsed EXACTLY once (single-flight) via a Logback ListAppender, not just 'all tokens verify'. - non-string reserved-key spoof (number/object/array) overwrite covered. - call(String) with no ToolContext proven fail-closed (no identity injected). - jti uniqueness across mints asserted (replay distinguishability). - JWT exp bounded to ttlSeconds window, not just 'after now'. - self-heal test now proves the bad-PEM failure is cached (not retried/cleared). - audienceFor by numeric id + name-precedence covered. - forwardsTo OR-semantics (id OR name) covered across all 4 combinations. - progress-path tests tightened to any(CallToolRequest.class). - colon-bearing visitorId plaintext round-trip pinned. - descriptive .as() messages on load-bearing assertions. Tests: 204 run, 0 failures (identity-forward subset 41→47).
…rdening
Addresses 9 confirmed findings from the third adversarial pass
(regression/interaction + production-readiness lenses) on the post-R2 code.
Deployment artifacts (P3-1 CRITICAL, P3-2/P3-3 HIGH) — the feature could NOT
be deployed via the documented Docker path before this:
- .env.example: added MCP_IDFWD_PRIVATE_KEY_PEM block (was absent; JWT_SECRET
was the precedent). An operator copying .env.example never learned the feature
existed, so token mode silently failed-closed forever.
- docker-compose.yml: pass MCP_IDFWD_PRIVATE_KEY_PEM into the container.
- application.yml: added a commented identity-forward example block so the
shipped config is self-documenting and the ${MCP_IDFWD_PRIVATE_KEY_PEM:}
placeholder actually exists (was only in docs/, which disagreed with the
shipped config).
Hardening:
- Log injection (P3-5): e.getMessage() from Jackson was interpolated raw into
logs; a crafted toolInput containing CR/LF could forge log lines. Added a
sanitize() helper (strip control chars, cap length) on all 3 message sites +
a test pinning the behavior.
- Encrypted/FIPS key errors (P3-8/P3-9): detect ENCRYPTED PRIVATE KEY and
NoSuchAlgorithmException specifically, emit actionable hints instead of a
generic 'failed to parse'.
Docs (en + zh):
- Clock skew (P3-6): document backend-side leeway / NTP guidance (60s TTL +
drift was rejecting valid tokens, undocumented).
- Key rotation runbook (P3-7): rotation is a coordinated multi-host operation
(update backends FIRST, then MateClaw, watch the load log); no JWKS yet.
Test quality:
- R3-2: deleted a dead 'captured' variable that misled about the assertion sink.
- R3-3: guarded the logback Logger casts with assumeTrue so a non-logback SLF4J
binding skips instead of throwing ClassCastException.
The signingKey() state machine (rewritten twice across R1/R2) was verified
correct across all 9 transitions by the regression reviewer; no production
regression found.
Tests: 204 run, 0 failures (identity-forward subset 47→48). YAML validated;
Spring loads application.yml; log-appender tests stable across stress runs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
对已合入
upstream/dev的 ISSUE #459 实现(STDIO MCP server on-behalf-of 身份透传,经 #460 / #471 / #464 合入)进行 3 轮对抗式代码审查,修复全部确认的问题。共 22 项发现,新增 31 个测试(身份透传子集 23 → 48,更广泛的回归 287 个测试全绿)。为什么需要这个 PR
ISSUE #459 已经合入
upstream/dev,但一轮聚焦的对抗式审查暴露了已合并代码中的真实缺陷:签名密钥轮换 bug、回放/排队消息路径丢失不可变用户 id、文档与代码漂移(会导致后端静默授权匿名访客)、一条安全关键但零测试的进度通知路径,以及——最关键的——部署产物缺失导致该功能根本无法通过文档描述的 Docker 路径部署。本 PR 修复全部问题并用测试锁定。3 轮审查详情
第 1 轮 — 安全 / 并发 / 契约一致性(8 项修复)
signingKey()缓存后短路 → 合法→合法的 PEM 轮换在重启前不生效(与 self-heal Javadoc 矛盾;在退役疑似泄露密钥时危险)。requesterUserId→ on-behalf-of 的sub退回到可变用户名并被标记为authenticated,而这恰恰是最需要 OBO 正确性的路径。已将requesterUserIdOf(auth)贯通到两条路径。<trust>:<subject>;JWT claims 列表漏掉了trust/channel_type;缺少 webchat/IM 渠道的警告说明。已修复中英文双版本 + Python 示例。sub语义写错(声称是requesterId;代码实际用不可变数字 idrequesterUserId)。ProgressAwareMcpToolCallback绕过了正常call())零行为测试。已补充明文/token/cron 场景测试。audienceFor(null,null)碰撞出"null"audience。synchronized块内修复。第 2 轮 — 健壮性 / 测试质量 / 集成(14 项修复)
Objects.equals+attemptedPem标志);坏 PEM 只解析/记录一次。signingKey()Javadoc 过度承诺了 "config reload / no restart" 的 self-heal 能力——McpIdentityForwardProperties是普通的@ConfigurationProperties(不是@RefreshScope,没有 spring-cloud),所以静态application.yml编辑仍需重启。已重写 Javadoc 陈述事实。ListAppender断言密钥只解析一次(single-flight);非字符串保留键伪造覆盖;call(String)无上下文 fail-closed;jti唯一性;expTTL 有界;self-heal 缓存失败;按 id 的audienceFor;forwardsToOR 语义;类型化 Mockito 匹配器;含冒号的 visitorId 往返;描述性.as()消息。第 3 轮 — 回归/交互 + 生产就绪(9 项修复)
.env.example中对该功能 /MCP_IDFWD_PRIVATE_KEY_PEM零引用——运维人员复制.env.example永远不会知道该功能的存在;token 模式会永远静默 fail-closed。已添加带说明的配置块(与JWT_SECRET风格一致)。docker-compose.yml没有把MCP_IDFWD_PRIVATE_KEY_PEM传入容器——Docker 环境下 token 模式无法工作。已添加透传。application.yml没有identity-forward配置块(只有mcp.enabled: true);文档与出厂配置不一致。已添加注释示例块,使出厂配置自文档化,$${MCP_IDFWD_PRIVATE_KEY_PEM:}占位符实际存在。e.getMessage()被原样插值;精心构造的含 CR/LF 的toolInput可以伪造日志行。已添加sanitize()辅助方法 + 测试。leeway+ NTP 指引(中英文)。captured变量;用assumeTrue保护 logback 强转。第 3 轮回归审查显式验证了
signingKey()状态机(重写两次)的全部 9 种状态转换,确认正确——R1/R2 的改动未引入生产回归。验证
JDK 21,
mvn -pl mateclaw-server test -Dtest='...'。有意推迟的项(附理由)
audienceFor返回Optional<String>而非抛异常——抛异常是正确的 fail-fast,且 Javadoc 文档化了生产路径保证;改动唯一调用方的价值不大。privateKeyPem上加@JsonIgnore——Spring Boot 默认的SanitizingFunction已自动遮蔽它(属性路径含 "key");此处添加会偏离仓库其他@ConfigurationProperties密钥(JWT_SECRET)依赖同一默认值的做法。提交记录
fix(mcp): round-1 adversarial review of #459 identity forwardingfix(mcp): round-2 adversarial review of #459 identity forwardingfix(mcp): round-3 adversarial review of #459 — deployment + hardening审查报告(ROUND-1/2/3.md)在审查过程中生成但未提交(它们是
.zcode/中的工作笔记)。本 PR 不关闭任何 issue(issue #459 已通过 #460/#471 关闭)。这是对已合并实现的加固。