From ba7116394733355c4856c615796ca44e524cd192 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Mon, 6 Nov 2023 10:52:18 -0800 Subject: [PATCH] Fix quoting of $ ` etc (#5) --- env.go | 24 ++++++++++++++++++------ env_test.go | 21 +++++++++++---------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/env.go b/env.go index 42f4697..60aab0a 100644 --- a/env.go +++ b/env.go @@ -88,13 +88,25 @@ func CamelCaseToLowerKebabCase(s string) string { return strings.ToLower(strings.Join(words, "-")) } +// Intermediate result list from StructToEnvVars(), both the Key and QuotedValue +// must be shell safe/non adversarial as they are emitted as is by String() with = in between. +// Using StructToEnvVars produces safe values even with adversarial input (length notwithstanding). type KeyValue struct { - Key string - Value string // Already quoted/escaped. + Key string // Must be safe (is when coming from Go struct names but could be bad with env:). + QuotedValue string // (Must be) Already quoted/escaped. +} + +// Escape characters such as the result string can be embedded as a single argument in a shell fragment +// e.g for ENV_VAR= such as is safe (no $(cmd...) no ` etc`). +func ShellQuote(input string) string { + // To emit a single quote in a single quote enclosed string you have to close the current ' then emit a quote (\'), + // then reopen the single quote sequence to finish. Note that when the string ends with a quote there is an unnecessary + // trailing ''. + return "'" + strings.ReplaceAll(input, "'", `'\''`) + "'" } func (kv KeyValue) String() string { - return fmt.Sprintf("%s=%s", kv.Key, kv.Value) + return fmt.Sprintf("%s=%s", kv.Key, kv.QuotedValue) } func ToShell(kvl []KeyValue) string { @@ -126,9 +138,9 @@ func SerializeValue(value interface{}) string { } return res case string: - return strconv.Quote(v) + return ShellQuote(v) default: - return strconv.Quote(fmt.Sprint(value)) + return ShellQuote(fmt.Sprint(value)) } } @@ -191,7 +203,7 @@ func structToEnvVars(envVars []KeyValue, allErrors []error, prefix string, s int value := fieldValue.Interface() stringValue = SerializeValue(value) } - envVars = append(envVars, KeyValue{Key: prefix + tag, Value: stringValue}) + envVars = append(envVars, KeyValue{Key: prefix + tag, QuotedValue: stringValue}) } return envVars, allErrors } diff --git a/env_test.go b/env_test.go index d1049db..4e53418 100644 --- a/env_test.go +++ b/env_test.go @@ -123,7 +123,7 @@ type FooConfig struct { func TestStructToEnvVars(t *testing.T) { intV := 199 foo := FooConfig{ - Foo: "a\nfoo with \" quotes and \\ and '", + Foo: "a\nfoo with $X, `backticks`, \" quotes and \\ and ' in middle and end '", Bar: "42str", Blah: 42, ABool: true, @@ -154,17 +154,18 @@ func TestStructToEnvVars(t *testing.T) { } str := ToShellWithPrefix("TST_", envVars) //nolint:lll - expected := `TST_FOO="a\nfoo with \" quotes and \\ and '" -TST_BAR="42str" -TST_A_SPECIAL_BLAH="42" + expected := `TST_FOO='a +foo with $X, ` + "`backticks`" + `, " quotes and \ and '\'' in middle and end '\''' +TST_BAR='42str' +TST_A_SPECIAL_BLAH='42' TST_A_BOOL=true -TST_HTTP_SERVER="http://localhost:8080" -TST_INT_POINTER="199" +TST_HTTP_SERVER='http://localhost:8080' +TST_INT_POINTER='199' TST_FLOAT_POINTER= -TST_INNER_A="inner a" -TST_INNER_B="inner b" -TST_RECURSE_HERE_INNER_A="rec a" -TST_RECURSE_HERE_INNER_B="rec b" +TST_INNER_A='inner a' +TST_INNER_B='inner b' +TST_RECURSE_HERE_INNER_A='rec a' +TST_RECURSE_HERE_INNER_B='rec b' export TST_FOO TST_BAR TST_A_SPECIAL_BLAH TST_A_BOOL TST_HTTP_SERVER TST_INT_POINTER TST_FLOAT_POINTER TST_INNER_A TST_INNER_B TST_RECURSE_HERE_INNER_A TST_RECURSE_HERE_INNER_B ` if str != expected {