diff --git a/internal/bugseti/ethics_guard.go b/internal/bugseti/ethics_guard.go index 8a267a7a..555ea138 100644 --- a/internal/bugseti/ethics_guard.go +++ b/internal/bugseti/ethics_guard.go @@ -106,7 +106,23 @@ func loadEthicsGuard(ctx context.Context, rootHint string) *EthicsGuard { } func (g *EthicsGuard) SanitizeEnv(value string) string { - return sanitizeInline(value, maxEnvRunes) + return stripShellMeta(sanitizeInline(value, maxEnvRunes)) +} + +// stripShellMeta removes shell metacharacters that could allow command +// injection when a value is interpolated inside a shell environment variable. +func stripShellMeta(s string) string { + var b strings.Builder + b.Grow(len(s)) + for _, r := range s { + switch r { + case '`', '$', ';', '|', '&', '(', ')', '{', '}', '<', '>', '!', '\\', '\'', '"', '\n', '\r': + continue + default: + b.WriteRune(r) + } + } + return strings.TrimSpace(b.String()) } func (g *EthicsGuard) SanitizeTitle(value string) string { diff --git a/internal/bugseti/ethics_guard_test.go b/internal/bugseti/ethics_guard_test.go index 0a4aaa2a..4784160a 100644 --- a/internal/bugseti/ethics_guard_test.go +++ b/internal/bugseti/ethics_guard_test.go @@ -1,6 +1,8 @@ package bugseti -import "testing" +import ( + "testing" +) func TestSanitizeInline_Good(t *testing.T) { input := "Hello world" @@ -26,3 +28,47 @@ func TestSanitizeMultiline_Ugly(t *testing.T) { t.Fatalf("expected %q, got %q", "ab\ncd", output) } } + +func TestSanitizeEnv_Good(t *testing.T) { + g := &EthicsGuard{} + input := "owner/repo-name" + output := g.SanitizeEnv(input) + if output != input { + t.Fatalf("expected %q, got %q", input, output) + } +} + +func TestSanitizeEnv_Bad(t *testing.T) { + g := &EthicsGuard{} + + tests := []struct { + name string + input string + expected string + }{ + {"backtick", "owner/repo`whoami`", "owner/repowhoami"}, + {"dollar", "owner/repo$(id)", "owner/repoid"}, + {"semicolon", "owner/repo;rm -rf /", "owner/reporm -rf /"}, + {"pipe", "owner/repo|cat /etc/passwd", "owner/repocat /etc/passwd"}, + {"ampersand", "owner/repo&&echo pwned", "owner/repoecho pwned"}, + {"mixed", "`$;|&(){}<>!\\'\"\n\r", ""}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + output := g.SanitizeEnv(tc.input) + if output != tc.expected { + t.Fatalf("expected %q, got %q", tc.expected, output) + } + }) + } +} + +func TestStripShellMeta_Ugly(t *testing.T) { + // All metacharacters should be stripped, leaving empty string + input := "`$;|&(){}<>!\\'\"" + output := stripShellMeta(input) + if output != "" { + t.Fatalf("expected empty string, got %q", output) + } +}