Skip to content

Harden sandbox.mcp.env key encoding at emission and launch boundaries - #53171

Merged
pelikhan merged 7 commits into
mainfrom
copilot/review-sandbox-mcp-env-encoding
Aug 16, 2026
Merged

Harden sandbox.mcp.env key encoding at emission and launch boundaries#53171
pelikhan merged 7 commits into
mainfrom
copilot/review-sandbox-mcp-env-encoding

Conversation

CopilotAI commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Review of the sandbox.mcp.env encoding path found that values are already injection-safe by construction (they travel through compiler-controlled GH_AW_MCP_GATEWAY_ENV_<n> step env vars and are reassembled into argv -e NAME=VALUE entries by a shell-less spawn), but keys relied entirely on upstream schema/frontmatter validation — nothing re-checked a name at the point where it becomes a Docker argument.

Encoding path reviewed

  • Schema: patternProperties^[A-Z_][A-Z0-9_]*$, string values, additionalProperties: false
  • validateSandboxConfig: rejects the reserved GH_AW_MCP_GATEWAY_* namespace and non-conforming names
  • writeMCPGatewayStepEnv: values written only under indexed transport names, YAML-escaped via %q; user names never become runner env var names
  • Docker command: carries only the __GH_AW_MCP_GATEWAY_CUSTOM_ENV__ marker — no key or value is interpolated into the run: block or the serialized command
  • start_mcp_gateway.cjs: rebuilds atomic -e NAME=VALUE argv entries, spawn without a shell

Changes

  • Compiler (pkg/workflow/mcp_setup_gateway.go): new sanitizedGatewayEnvNames filters custom env names against ^[A-Z_][A-Z0-9_]*$ and the reserved transport namespace. The names manifest, indexed transport values, override suppression, and the Docker marker now all derive from this single sanitized set, so a name carrying shell/Docker metacharacters cannot be emitted even if an earlier validation path is bypassed. The env: block is omitted when nothing valid remains.
  • Launcher (actions/setup/js/start_mcp_gateway.cjs): injectCustomGatewayEnvArgs additionally rejects reserved-namespace names and duplicates; the manifest variable and transport prefix are now shared constants used in lookups and error messages.
  • Tests: Go coverage for name filtering and marker omission; JS coverage for reserved and duplicate names; launcher-contract assertions updated to the new constants.
# Names that survive schema/validation are re-checked before emission;# only well-formed, non-reserved names reach the transport manifest.sandbox:
mcp:
env:
API_TOKEN: ${{ secrets.API_TOKEN }} # -> GH_AW_MCP_GATEWAY_ENV_0

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 14.2 AIC · ⌖ 6.77 AIC · ⊞ 6.3K ·
Comment /souschef to run again

CopilotAIand others added 2 commits August 16, 2026 18:20
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
CopilotAI requested a review from pelikhanAugust 16, 2026 18:21
@pelikhan
pelikhan marked this pull request as ready for review August 16, 2026 18:22
CopilotAI balanced review requested due to automatic review settings August 16, 2026 18:22
@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR does not have the 'implementation' label and has ≤100 new lines of code in business logic directories (82 additions detected).

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Ponytail Reviewer completed successfully!

Generated by Ponytail Reviewer for #53171

@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel skipped because pre-fetch PR data was unavailable: unable to fetch test file diff

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actionsBot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

Copy link
Copy Markdown
Contributor

Comment Memory

reviewed_at: 2026-08-16T18:24:00Z
review_event: COMMENT
top_themes:
- no actionable blocking issues in changed lines
- defense-in-depth validation for MCP gateway env names
files_reviewed:
- actions/setup/js/start_mcp_gateway.cjs
- actions/setup/js/start_mcp_gateway.test.cjs
- pkg/workflow/mcp_gateway_env_security_test.go
- pkg/workflow/mcp_setup_gateway.go
comment_count: 0

Note

This comment is managed by comment memory.

It stores persistent context for this thread in the code block at the top of this comment.
Edit only the text inside the backtick fences; workflow metadata and the footer are regenerated automatically.

Learn more about comment memory

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 4.75 AIC · ⌖ 6.35 AIC · ⊞ 4.5K ·
Comment /review to run again

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: comment

No blocking issues stood out in the changed lines. This is a narrowly scoped defense-in-depth hardening pass, and the compiler/runtime checks stay aligned.

Highlights
  • The compiler now derives marker emission, override suppression, and transport slot emission from the same sanitized name set, which closes the obvious "validation bypass" gap.
  • The launcher-side duplicate and reserved-name rejection is a sensible backstop and is covered by targeted tests.
  • I did not find a changed-line correctness, crash, or regression issue strong enough to block merge.

🔎 Code quality review by PR Code Quality Reviewer · gpt54 · 4.75 AIC · ⌖ 6.35 AIC · ⊞ 4.5K
Comment /review to run again

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Hardens MCP gateway environment-variable handling at compiler emission and runtime launch boundaries.

Changes:

  • Filters unsafe or reserved custom environment names during compilation.
  • Rejects reserved and duplicate names at runtime.
  • Adds Go and JavaScript security coverage.
Show a summary per file
FileDescription
pkg/workflow/mcp_setup_gateway.goAdds emission-boundary filtering.
pkg/workflow/mcp_gateway_env_security_test.goTests filtering and marker omission.
actions/setup/js/start_mcp_gateway.cjsAdds launcher validation.
actions/setup/js/start_mcp_gateway.test.cjsTests reserved and duplicate names.

Review details

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment on lines +100 to +101
if (names.some(name => name === customGatewayEnvNamesVar || name.startsWith(customGatewayEnvTransportPrefix))) {
throw new Error(`${customGatewayEnvNamesVar} must not contain names reserved by the ${customGatewayEnvTransportPrefix} transport namespace`);
Comment threadpkg/workflow/mcp_setup_gateway.go Outdated
func sanitizedGatewayEnvNames(gatewayEnvVars map[string]string) []string {
names := make([]string, 0, len(gatewayEnvVars))
for _, name := range sliceutil.SortedKeys(gatewayEnvVars) {
if isReservedMCPGatewayTransportEnvVar(name) {

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Harden sandbox.mcp.env key encoding at emission and launch boundaries

The PR correctly centralises the name-sanitisation logic in sanitizedGatewayEnvNames and applies it at both the YAML emission boundary and the marker-injection boundary. The JS-side validation additions (reserved-name rejection, duplicate rejection) and the new tests are thorough and well-structured.

One bug found

buildAddedGatewayEnvVarSet (Go, line 715) does not use the sanitised name list.

The loop at lines 715–717 still iterates over gatewayConfig.Env (all raw names), so reserved and invalid names remain in the addedEnvVars deduplication set. This inconsistency means an HTTP MCP env var whose name coincidentally matches a user-supplied invalid/reserved gateway env var name would be silently suppressed from docker run -e flags, even though the gateway var itself was already filtered out by sanitizedGatewayEnvNames. Inline comment filed.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 29.2 AIC · ⌖ 7.22 AIC · ⊞ 5.6K

Comments that could not be inline-anchored

pkg/workflow/mcp_setup_gateway.go:716

Bug: buildAddedGatewayEnvVarSet uses raw (unsanitized) gatewayConfig.Env keys

This loop (lines 715–717) iterates over gatewayConfig.Env directly, so reserved names (GH_AW_MCP_GATEWAY_CUSTOM_ENV_NAMES, GH_AW_MCP_GATEWAY_ENV_*) and structurally invalid names are still added to addedEnvVars. Those names are already filtered out by sanitizedGatewayEnvNames before emission, but remain in the "already added" deduplication set used to suppress HTTP MCP env var forwarding.

Consequen…

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ponytail review (over-engineering only).

One finding — see inline comment on pkg/workflow/mcp_setup_gateway.go.

net: -3 lines possible.

Generated by ✂️ Ponytail Reviewer for #53171 · auto · 34.5 AIC · ⌖ 2.81 AIC · ⊞ 7.2K
Comment /ponytail to run again

}
yaml.WriteString(" env:\n")
customEnvVarNames := sliceutil.SortedKeys(gatewayEnvVars)
overriddenNames := make(map[string]struct{}, len(customEnvVarNames))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L93-96: yagni: builds a map[string]struct{} + setutil.Contains just to test membership in a small slice already in scope. slices.Contains(customEnvVarNames, envVarName) (package already imported), 0 extra lines.

github-actions[bot]
github-actionsBot previously requested changes Aug 16, 2026

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on three focused issues; the security logic itself is solid.

📋 Key Themes & Highlights

Key Themes

  • Hardcoded constant values in JS test (start_mcp_gateway.test.cjs:86): it.each uses string literals instead of the constants customGatewayEnvNamesVar / customGatewayEnvTransportPrefix, so a rename will silently leave the test passing with stale expectations.
  • Double-call to sanitizedGatewayEnvNames (mcp_setup_gateway.go:657): the marker-guard call and the emission path independently re-derive the sanitized slice from the same map. No correctness bug today, but a future mutation between the two calls could produce a marker↔env-var mismatch that is hard to diagnose.
  • Overly broad assertion in TestMCPGatewayCustomEnvMarkerOmittedWhenAllNamesFiltered: assert.Empty on the whole builder output obscures intent and could mask a regression if HTTP-MCP env flags are added to the same builder.

Positive Highlights

  • ✅ Defense-in-depth approach is well-conceived: re-validating names at the final emission boundary before Docker args are constructed is exactly the right place.
  • sanitizedGatewayEnvNames is clean, well-documented, and properly covers both the pattern check and the reserved-namespace check.
  • ✅ Go contract test (TestMCPGatewayCustomEnvCommandContract) is an excellent technique for keeping compiler and launcher constants in sync.
  • ✅ Injection-payload names in TestMCPGatewayCustomEnvNamesAreFilteredAtEmissionBoundary provide strong regression protection.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 · 54 AIC · ⌖ 8.84 AIC · ⊞ 7.7K
Comment /matt to run again

).toThrow(/valid environment variable names/);
});

it.each([["GH_AW_MCP_GATEWAY_ENV_0"], ["GH_AW_MCP_GATEWAY_CUSTOM_ENV_NAMES"]])("rejects the reserved transport name %s", reservedName => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Test uses hardcoded string literals ("GH_AW_MCP_GATEWAY_ENV_0", "GH_AW_MCP_GATEWAY_CUSTOM_ENV_NAMES") rather than the module constants. If either constant value changes, this test will silently pass while the behaviour diverges.

💡 Suggestion

Export the constants from start_mcp_gateway.cjs and import them in the test:

// start_mcp_gateway.cjs — expose constantsmodule.exports.customGatewayEnvNamesVar=customGatewayEnvNamesVar;module.exports.customGatewayEnvTransportPrefix=customGatewayEnvTransportPrefix;// test fileit.each([[`${customGatewayEnvTransportPrefix}0`],[customGatewayEnvNamesVar]])('rejects the reserved transport name %s',reservedName=>{/* ... */});

The Go contract test already verifies constant values end-to-end, so this change would make the JS test self-consistent with the same discipline.

@copilot please address this.

Comment threadpkg/workflow/mcp_setup_gateway.go Outdated

func appendMCPGatewayCustomAndHTTPEnvFlags(containerCmd *strings.Builder, workflowData *WorkflowData, gatewayConfig *MCPGatewayRuntimeConfig, mcpEnvVars map[string]string, hasGitHub bool, githubTool map[string]any, tools map[string]any, engine CodingAgentEngine) {
if len(gatewayConfig.Env) > 0 {
if len(sanitizedGatewayEnvNames(gatewayConfig.Env)) > 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs]sanitizedGatewayEnvNames is called twice for every gateway config that has env vars: once here to decide whether to write the marker, and once inside writeMCPGatewayStepEnv (indirectly through appendMCPGatewayCustomAndHTTPEnvFlagswriteMCPGatewayStepEnv). The double-call is cheap now but creates a subtle maintenance trap — a future caller that mutates the map between calls could produce a marker with no corresponding env vars (or vice-versa).

💡 Suggestion

Consider caching the sanitized slice on MCPGatewayRuntimeConfig (or threading it as a parameter) so the guard and the emission path share a single computation:

// In the calling site:sanitized:=sanitizedGatewayEnvNames(gatewayConfig.Env)
iflen(sanitized) >0 {
containerCmd.WriteString(" "+mcpGatewayCustomEnvMarker)
}
// pass sanitized into writeMCPGatewayStepEnv instead of re-deriving it

This also makes it easier to write a test that asserts marker presence ↔ non-empty sanitized names in one place.

@copilot please address this.

gatewayEnv := map[string]string{"BAD-NAME": "shell-unsafe"}

var yaml strings.Builder
writeMCPGatewayStepEnv(&yaml, nil, nil, gatewayEnv)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd]TestMCPGatewayCustomEnvMarkerOmittedWhenAllNamesFiltered verifies that the container command string is empty when all names are invalid. However the assertion assert.Empty(t, containerCommand.String()) checks the entire container command output — if appendMCPGatewayCustomAndHTTPEnvFlags ever emits non-custom content before or after the marker check (e.g. HTTP MCP env flags added to the same builder), the test could mask a regression where the marker is still written.

💡 Suggestion

Assert specifically that the marker constant is absent rather than that the whole output is empty:

assert.NotContains(t, containerCommand.String(), mcpGatewayCustomEnvMarker,
"marker must be omitted when all gateway env names are filtered")

This makes the assertion more intent-revealing and resilient to future additions.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot this PR is ready for the next finishing pass.

Please address these items, newest first:

  • Harden sandbox.mcp.env key encoding at emission and launch boundaries #53171 (comment)buildAddedGatewayEnvVarSet still uses raw gateway env keys; switch it to the sanitized name set so filtered names cannot suppress unrelated HTTP MCP env forwarding.
  • Review thread from Matt Pocock Skills Reviewer — replace hardcoded JS test literals with customGatewayEnvNamesVar / customGatewayEnvTransportPrefix so the contract test fails on constant renames.
  • Review thread from Matt Pocock Skills Reviewer — avoid the double derivation of sanitizedGatewayEnvNames, or otherwise ensure marker emission and env-var emission share one computed result.
  • Review thread from Matt Pocock Skills Reviewer — tighten TestMCPGatewayCustomEnvMarkerOmittedWhenAllNamesFiltered so it asserts only the marker omission instead of the whole builder being empty.

Failed checks to fix:

Please refresh the branch state if needed, rerun the relevant tests, and run the pr-finisher skill before handing back to maintainers.

Generated by 👨🍳 PR Sous Chef

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 14.2 AIC · ⌖ 6.77 AIC · ⊞ 6.3K ·
Comment /souschef to run again

CopilotAIand others added 2 commits August 16, 2026 19:12
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

CopilotAI commented Aug 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot this PR is ready for the next finishing pass.

Please address these items, newest first:...

Addressed in 29b8ce8 and 5eb4495: custom-name filtering is computed once for compiler emission and marker generation, filtered keys no longer suppress HTTP MCP forwarding, launcher tests use exported transport constants, and marker/step-env omission coverage is focused. Local validation passed; the listed CI jobs failed before tests on a missing cache entry.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot thanks for the follow-up. Please run the pr-finisher skill, verify all outstanding review threads are resolved, refresh the branch if needed, and rerun the relevant CI so maintainers can re-check quickly.

Generated by 👨🍳 PR Sous Chef

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 7.6 AIC · ⌖ 7.29 AIC · ⊞ 6.3K ·
Comment /souschef to run again

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

CopilotAI commented Aug 16, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot thanks for the follow-up. Please run the pr-finisher skill, verify all outstanding review threads are resolved, refresh the branch if needed, and rerun the relevant CI so maintainers can re...

Resolved the remaining full-namespace gap in f4b3a64: compiler and launcher now both reject every GH_AW_MCP_GATEWAY_ name, with Go and JS coverage. Ran the PR-finisher validation locally (make fmt, make lint, make test-unit, make test, make recompile); all passed. The prior listed CI jobs failed before tests on a required cache miss. Cloud-agent pushes cannot re-trigger CI, so a maintainer must re-run it for this HEAD; review-thread resolution is not exposed to this agent.

…x-mcp-env-encoding
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot thanks for the follow-up. This PR looks close. Please run the pr-finisher skill, verify any outstanding review threads are resolved, refresh the branch if needed, and rerun the relevant CI so maintainers can re-check quickly.

No new failed checks were listed in the compact candidate data for this run.

Generated by 👨🍳 PR Sous Chef
Comment /souschef to run again

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 5.37 AIC · ⌖ 7.87 AIC · ⊞ 6.3K ·
Comment /souschef to run again

@pelikhan
pelikhan merged commit 1c8b111 into mainAug 16, 2026
36 of 37 checks passed
@pelikhan
pelikhan deleted the copilot/review-sandbox-mcp-env-encoding branch August 16, 2026 21:50
Copilot stopped work on behalf of gh-aw-bot due to an error August 16, 2026 21:50
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.87.1

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@gh-aw-bot@pelikhan