From c665ed39ae2d59783ba3a8e24aa843e7e174af8c Mon Sep 17 00:00:00 2001 From: Scott Vitale Date: Tue, 21 Jul 2026 18:54:41 -0600 Subject: [PATCH] fix: escape SSM parameter values in eval-mode output to prevent shell injection SECOPS-25088: unescaped SSM parameter value in eval-mode output allows shell command execution on the consuming host. When aws-env runs without a command argument it prints export statements for use with eval/source: export VAR=$'' $'...' (ANSI-C quoting) terminates at an unescaped single quote. A malicious or attacker-controlled SSM value containing a single quote could break out of the quoting context and inject arbitrary shell commands on the consuming host. For example, a value of foo'; rm -rf /; echo ' would produce: export VAR=$'foo'; rm -rf /; echo '' which executes rm -rf / when eval'd. The fix escapes backslashes (\ -> \\) before single quotes (' -> \') within the ANSI-C quoted string, so special characters are preserved as literal values. The backslash pass must come first to avoid double-escaping characters that were already backslashes in the input. Co-Authored-By: Claude Sonnet 4.6 --- cmd/aws-env/main.go | 11 ++++++++- cmd/aws-env/main_test.go | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 cmd/aws-env/main_test.go diff --git a/cmd/aws-env/main.go b/cmd/aws-env/main.go index cc7ec51..16ff545 100644 --- a/cmd/aws-env/main.go +++ b/cmd/aws-env/main.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "os/signal" + "strings" "syscall" "github.com/sendgrid/aws-env/awsenv" @@ -198,7 +199,7 @@ func dump(r *awsenv.Replacer) error { for name, newVal := range vars { log.WithField("envvar", name).Info("replacing") - fmt.Printf("export %s=$'%s'\n", name, newVal) + fmt.Printf("export %s=$'%s'\n", name, ansiCEscape(newVal)) } return nil @@ -256,6 +257,14 @@ func invoke(r *awsenv.Replacer, prog string, args []string) error { } } +// ansiCEscape escapes a string for safe use inside $'...' (ANSI-C quoting). +// Backslashes must be escaped first to avoid double-escaping. +func ansiCEscape(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `'`, `\'`) + return s +} + func main() { if err := app.Run(os.Args); err != nil { log.WithError(err).Fatalf("%s failed to start", app.Name) diff --git a/cmd/aws-env/main_test.go b/cmd/aws-env/main_test.go new file mode 100644 index 0000000..cbfedf4 --- /dev/null +++ b/cmd/aws-env/main_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "testing" +) + +func TestAnsiCEscape(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "plain value", + input: "hello", + want: "hello", + }, + { + name: "single quote injection", + input: "foo'; rm -rf /; echo '", + want: `foo\'; rm -rf /; echo \'`, + }, + { + name: "backslash", + input: `foo\bar`, + want: `foo\\bar`, + }, + { + name: "backslash before single quote", + input: `foo\'bar`, + want: `foo\\\'bar`, + }, + { + name: "empty string", + input: "", + want: "", + }, + { + name: "value with newline", + input: "line1\nline2", + want: "line1\nline2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ansiCEscape(tt.input) + if got != tt.want { + t.Errorf("ansiCEscape(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +}