diff --git a/platform/internal/handlers/template_import.go b/platform/internal/handlers/template_import.go index 3d3be73f2..cd6bafc2a 100644 --- a/platform/internal/handlers/template_import.go +++ b/platform/internal/handlers/template_import.go @@ -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") diff --git a/platform/internal/handlers/template_import_test.go b/platform/internal/handlers/template_import_test.go index 0a792059f..9fe9c1d77 100644 --- a/platform/internal/handlers/template_import_test.go +++ b/platform/internal/handlers/template_import_test.go @@ -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) {