From 40f33758ccbba17e634b3c2355f7b2ea777e787d Mon Sep 17 00:00:00 2001 From: Security Engineer Date: Sun, 13 Sep 2026 11:25:50 +0000 Subject: [PATCH] test(security): pin `command` by value at both workspaceRuntime door guards (PEN-3130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The door #12 and #12b masking guards in `server/src/__tests__/issues-goal-context-routes.test.ts` asserted three fixture keys but pinned only two by value. `command` was constrained solely by its presence in the `Object.keys(...)` set, so a regression that passed the operator-authored command line through verbatim kept the key set byte-identical and the token redacted — and stayed green. Both fixtures carry their secret in the token field (`GRAFANA_API_TOKEN` / `DEPLOY_TOKEN`) alongside a benign `command: "pnpm dev"`, so the sibling `expect(JSON.stringify(res.body)).not.toContain(SECRET)` guard cannot see a command-only pass-through either. That is the actual hole. No production change: `command` is masked today. It is not in `WORKSPACE_RUNTIME_IDENTITY_KEYS` (`server/src/redaction.ts:792`) nor in `WORKSPACE_RUNTIME_ENUM_KEYS` (`:794`), so in `maskEntry` it takes neither the identity branch (`:855`) nor the enum branch (`:859`) and falls to `:864`, where a string is masked to `***REDACTED***`. These two lines pin that existing behaviour. Mutation proof, recorded per test rather than as a suite total, two columns: (a) `command` added to `WORKSPACE_RUNTIME_IDENTITY_KEYS` — all three door tests red on the new lines (`expected 'pnpm dev' to be '***REDACTED***'`), AND `redaction.test.ts` "masks a service entry's free-text command and cwd" red. This mutation was already caught corpus-wide, so it is NOT what justifies this change. (b) a projection-layer re-attach at `server/src/routes/issues.ts:7503` and `:7622` — a plausible "keep the command legible in the UI" edit that restores `command` after `maskWorkspaceRuntimeForRead` returns. With these lines: 3 door tests red. Without them: door tests 15/15 green and `redaction.test.ts` 63/63 green, while the operator command line crosses the boundary verbatim. `redaction.test.ts` cannot see this because it calls the walk directly and never goes through the route. Column (b) is the regression class these lines uniquely catch. `redaction.test.ts` is deliberately not touched: its `:996` already pins `command` by value at the walk level, so a line there would be redundant. Unmutated: 15/15 pass, `tsc --noEmit` exit 0. Refs PEN-3130 Signed-off-by: Security Engineer --- .../src/__tests__/issues-goal-context-routes.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/__tests__/issues-goal-context-routes.test.ts b/server/src/__tests__/issues-goal-context-routes.test.ts index e706619e00f4..765985bae8ac 100644 --- a/server/src/__tests__/issues-goal-context-routes.test.ts +++ b/server/src/__tests__/issues-goal-context-routes.test.ts @@ -736,6 +736,13 @@ describe.sequential("issue goal context routes", () => { // Structure and names survive so the config stays legible; values do not. expect(runtime.services[0].name).toBe("api"); expect(Object.keys(runtime.services[0])).toEqual(["name", "command", "GRAFANA_API_TOKEN"]); + // PEN-3130. `command` is pinned by VALUE, not merely by its presence in the key + // set above. A projection edit that re-attached the operator-authored command + // line after masking — "keep the command legible in the UI" — leaves that key set + // byte-identical, keeps the token redacted, and keeps the `not.toContain` guard + // green (this fixture's secret lives in the token, not the command). Without this + // line nothing at this boundary would see it. + expect(runtime.services[0].command).toBe("***REDACTED***"); expect(runtime.services[0].GRAFANA_API_TOKEN).toBe("***REDACTED***"); }); } @@ -800,6 +807,10 @@ describe.sequential("issue goal context routes", () => { // Names and structure survive so the config stays legible; values do not. expect(Object.keys(runtime.services[0])).toEqual(["name", "command", "DEPLOY_TOKEN"]); expect(runtime.services[0].name).toBe("web"); + // PEN-3130, same reasoning as the execution-workspace door above: pinned by + // value so a command-only pass-through at this projection cannot hide behind + // an unchanged key set and a still-redacted token. + expect(runtime.services[0].command).toBe("***REDACTED***"); expect(runtime.services[0].DEPLOY_TOKEN).toBe("***REDACTED***"); // `desiredState` is enum-validated by the reader, so it must NOT be // masked — this pins the fix to the open field instead of the whole object.