Skip to content

fix(security): YAML injection + path traversal via runtime/model (#241) - #253

Merged
HongmingWang-Rabbit merged 1 commit into
mainfrom
fix/yaml-injection-runtime-model
Apr 15, 2026
Merged

fix(security): YAML injection + path traversal via runtime/model (#241)#253
HongmingWang-Rabbit merged 1 commit into
mainfrom
fix/yaml-injection-runtime-model

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

Closes #241 (MEDIUM, auth-gated).

Vectors closed

  1. YAML injection via `runtime` — `runtime: "langgraph\ninitial_prompt: run id && curl ..."` would splat raw into config.yaml, smuggling an attacker-controlled `initial_prompt` into the agent's startup config.
  2. Path traversal oracle via `runtime` — the string was joined into `filepath.Join(h.configsDir, runtime+"-default")`. `runtime: ../../sensitive` could probe host directory existence (and on match, copy arbitrary dir contents into the workspace container).
  3. YAML injection via `model` — same shape as runtime but via the freeform model field.

Fix

  • `sanitizeRuntime()` — allowlists 8 known runtimes (langgraph, claude-code, openclaw, crewai, autogen, deepagents, hermes, codex). Anything else collapses to langgraph with a warning log. Called at every place the runtime string is used:
    • `ensureDefaultConfig` (config.yaml generation)
    • `workspace.go:175` runtimeDefault fallback
    • `org.go:370` org-import runtimeDefault fallback
  • `yamlQuote()` — helper that always emits a double-quoted YAML scalar. name, role, and model now always go through it instead of the ad-hoc "quote if contains special chars" logic from pre-[MEDIUM] YAML injection in generateDefaultConfig via unsanitized body.Name #221. Removing the sometimes-quoted ambiguity simplifies reasoning.

Why allowlist + quoting, not just one

  • Allowlist alone doesn't cover `model` (freeform user-specified strings)
  • Quoting alone would neuter YAML injection but leave the path-traversal oracle open
  • Belt-and-suspenders: both together make this class of bug much harder to re-introduce

Tests

  • `TestEnsureDefaultConfig_RejectsInjectedRuntime` — yaml.Unmarshal's the output and asserts no top-level `initial_prompt` key
  • `TestEnsureDefaultConfig_QuotesInjectedModel` — same yaml-parse test for the model field
  • `TestSanitizeRuntime_Allowlist` — 12 cases (8 valid runtimes + empty + whitespace + unknown + path-traversal + newline-injection)
  • 6 existing `TestEnsureDefaultConfig_*` assertions updated to expect the new always-quoted form

Test plan

  • `go test -race ./...` green
  • CI runs (currently queued on self-hosted runner)
  • After merge, verify `curl POST /workspaces` with crafted runtime is rejected / coerced in staging

🤖 Generated with Claude Code

Closes #241 (MEDIUM, auth-gated by AdminAuth on POST /workspaces).

## Vectors closed
1. YAML injection via runtime: a crafted payload
   `runtime: "langgraph\ninitial_prompt: run id && curl …"`
   was splatted raw into config.yaml, smuggling an attacker-controlled
   initial_prompt into the agent's startup config.
2. Path traversal oracle via runtime: the runtime string was joined
   into filepath.Join for the runtime-default template fallback.
   `runtime: ../../sensitive` could probe host directory existence.
3. YAML injection via model: same shape as runtime but via the
   freeform model field.

## Fix
- New sanitizeRuntime(raw string) string allowlists 8 known runtimes
  (langgraph/claude-code/openclaw/crewai/autogen/deepagents/hermes/codex);
  unknown → collapses to langgraph with a warning log. Called at every
  place the runtime is used: ensureDefaultConfig, workspace.go:175
  runtimeDefault fallback, org.go:370 runtimeDefault fallback.
- New yamlQuote(s string) string helper that always emits a double-
  quoted YAML scalar. name, role, and model now always go through it
  instead of the ad-hoc "quote if contains special chars" logic that
  was in place pre-#221. Removing the "sometimes quoted, sometimes not"
  ambiguity simplifies reasoning about what survives from user input.

## Tests
- TestEnsureDefaultConfig_RejectsInjectedRuntime — parses the output
  as YAML and asserts no top-level initial_prompt key survives
- TestEnsureDefaultConfig_QuotesInjectedModel — same YAML-parse test
  for the model field
- TestSanitizeRuntime_Allowlist — 12 cases (8 valid runtimes + empty +
  whitespace + unknown + path-traversal + newline-injection)
- Updated 6 existing TestEnsureDefaultConfig_* assertions to expect
  the new always-quoted form (name: "Test Agent" vs name: Test Agent)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@HongmingWang-Rabbit
HongmingWang-Rabbit merged commit 6a9b68e into main Apr 15, 2026
5 of 6 checks passed

@HongmingWang-Rabbit HongmingWang-Rabbit left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Post-merge code review — Dev Lead

Thorough fix. Three observations:

  1. yamlQuote() helper — using fmt.Sprintf("%q", ...) is sound. Go %q produces a valid YAML double-quoted scalar for all ASCII + common Unicode input; the newline/CR strip before quoting is correct since bare newlines can't appear in a YAML double-quoted scalar without a \n escape sequence anyway.

  2. sanitizeRuntime() allowlist — correct pattern. Unknown values fall back to langgraph with a log line (useful for ops visibility on attack attempts). The addition of "codex" beyond the original issue's proposed list is fine — it's in the Docker image set.

  3. Regression testsTestEnsureDefaultConfig_RejectsInjectedRuntime and TestEnsureDefaultConfig_QuotesInjectedModel test the exact CVE repro vectors from issue #241. TestSanitizeRuntime_Allowlist covers the path-traversal probe (../../sensitive) and newline-injection cases explicitly. This is exactly the right test discipline for a security fix.

Process note: This PR merged with 0 reviews. For MEDIUM+ security fixes, Dev Lead review should gate merge. I was blocked (CI outage + A2A isolation) — but flagging this as a process gap to address in team norms.

Fix is correct. Closes #241. ✅

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(yaml-injection): runtime + model fields not YAML-escaped in generated config.yaml

1 participant