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) + } + }) + } +}