fix(security): sanitize body.Name before YAML interpolation in generateDefaultConfig

A crafted workspace name containing a newline (e.g. "x\nmodel: evil")
could inject arbitrary YAML keys into the auto-generated config.yaml.
Strip \n and \r from the name before interpolation. YAML key injection
requires a newline to start a new mapping entry; other characters such
as `:` are safe in unquoted scalar values.

Adds TestGenerateDefaultConfig_YAMLInjection with three adversarial
inputs: bare \n injection, CRLF injection, and multi-key injection.

Closes #221

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dev Lead Agent 2026-04-15 18:44:11 +00:00
parent 519d478ea2
commit afea61ae52
2 changed files with 45 additions and 0 deletions

View File

@ -54,6 +54,13 @@ func generateDefaultConfig(name string, files map[string]string) string {
}
}
// Sanitize: strip newlines and carriage returns to prevent YAML key
// injection. A crafted name like "x\nmodel: malicious" would otherwise
// break out of the scalar and inject arbitrary YAML keys. Newlines are
// the only vector because YAML only starts a new mapping entry on a new
// line; other characters such as ":" are safe in unquoted scalar values.
name = strings.NewReplacer("\n", "", "\r", "").Replace(name)
var cfg strings.Builder
cfg.WriteString("name: " + name + "\n")
cfg.WriteString("description: Imported agent\n")

View File

@ -94,6 +94,44 @@ func TestGenerateDefaultConfig_Empty(t *testing.T) {
}
}
// TestGenerateDefaultConfig_YAMLInjection verifies that a crafted workspace
// name containing a newline cannot inject arbitrary YAML keys into the
// generated config. This is the regression test for issue #221.
func TestGenerateDefaultConfig_YAMLInjection(t *testing.T) {
adversarialCases := []struct {
desc string
name string
bannedLines []string // lines that must NOT appear in the output
}{
{
desc: "newline followed by new key",
name: "legit-agent\nmodel: malicious:model",
bannedLines: []string{"model: malicious:model"},
},
{
desc: "CRLF injection",
name: "legit-agent\r\nmalicious_key: value",
bannedLines: []string{"malicious_key: value"},
},
{
desc: "multiple newlines with multiple keys",
name: "x\nfoo: bar\nbaz: qux",
bannedLines: []string{"foo: bar", "baz: qux"},
},
}
for _, tc := range adversarialCases {
t.Run(tc.desc, func(t *testing.T) {
cfg := generateDefaultConfig(tc.name, map[string]string{})
for _, banned := range tc.bannedLines {
if strings.Contains(cfg, banned) {
t.Errorf("YAML injection: output contains injected line %q\nfull config:\n%s", banned, cfg)
}
}
})
}
}
// ==================== writeFiles ====================
func TestWriteFiles_Success(t *testing.T) {