From 6066db86e93caeb8132a64dbfd5f5ce94cba7514 Mon Sep 17 00:00:00 2001 From: Nimish Date: Fri, 13 Mar 2026 18:36:25 +0800 Subject: [PATCH] fix: escape special characters in export formats TOML and dotenv values with embedded double quotes or backslashes produced invalid output. XML key names in the name attribute were not escaped. HCL was missing backslash escaping. --- src/pkg/util/export.go | 21 ++++++--- src/pkg/util/export_test.go | 92 +++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/src/pkg/util/export.go b/src/pkg/util/export.go index db81fcb1..63fe5d73 100644 --- a/src/pkg/util/export.go +++ b/src/pkg/util/export.go @@ -19,7 +19,10 @@ type KeyValue struct { func ExportDotenv(secrets []KeyValue) { for _, kv := range secrets { - fmt.Printf("%s=\"%s\"\n", kv.Key, kv.Value) + escaped := strings.ReplaceAll(kv.Value, "\\", "\\\\") + escaped = strings.ReplaceAll(escaped, "\"", "\\\"") + escaped = strings.ReplaceAll(escaped, "\n", "\\n") + fmt.Printf("%s=\"%s\"\n", kv.Key, escaped) } } @@ -79,22 +82,28 @@ func ExportYAML(secrets []KeyValue) { func ExportXML(secrets []KeyValue) { fmt.Println("") for _, kv := range secrets { - var escaped strings.Builder - xml.EscapeText(&escaped, []byte(kv.Value)) - fmt.Printf(" %s\n", kv.Key, escaped.String()) + var escapedKey strings.Builder + xml.EscapeText(&escapedKey, []byte(kv.Key)) + nameAttr := strings.ReplaceAll(escapedKey.String(), "\"", """) + var escapedVal strings.Builder + xml.EscapeText(&escapedVal, []byte(kv.Value)) + fmt.Printf(" %s\n", nameAttr, escapedVal.String()) } fmt.Println("") } func ExportTOML(secrets []KeyValue) { for _, kv := range secrets { - fmt.Printf("%s = \"%s\"\n", kv.Key, kv.Value) + escaped := strings.ReplaceAll(kv.Value, "\\", "\\\\") + escaped = strings.ReplaceAll(escaped, "\"", "\\\"") + fmt.Printf("%s = \"%s\"\n", kv.Key, escaped) } } func ExportHCL(secrets []KeyValue) { for _, kv := range secrets { - escaped := strings.ReplaceAll(kv.Value, "\"", "\\\"") + escaped := strings.ReplaceAll(kv.Value, "\\", "\\\\") + escaped = strings.ReplaceAll(escaped, "\"", "\\\"") fmt.Printf("variable \"%s\" {\n", kv.Key) fmt.Printf(" default = \"%s\"\n", escaped) fmt.Println("}") diff --git a/src/pkg/util/export_test.go b/src/pkg/util/export_test.go index 0ef40716..df0c8098 100644 --- a/src/pkg/util/export_test.go +++ b/src/pkg/util/export_test.go @@ -174,6 +174,98 @@ func TestExportDotenvAndKVLikeFormats(t *testing.T) { } } +func TestExportDotenv_Escaping(t *testing.T) { + secrets := []KeyValue{ + {Key: "SIMPLE", Value: "hello"}, + {Key: "WITH_QUOTES", Value: `he said "hello"`}, + {Key: "WITH_BACKSLASH", Value: `path\to\file`}, + {Key: "WITH_NEWLINE", Value: "line1\nline2"}, + {Key: "WITH_ALL", Value: "say \"hi\"\nand \\go"}, + } + out := captureStdout(t, func() { ExportDotenv(secrets) }) + + if !strings.Contains(out, `SIMPLE="hello"`) { + t.Fatalf("simple value wrong: %s", out) + } + if !strings.Contains(out, `WITH_QUOTES="he said \"hello\""`) { + t.Fatalf("quoted value not escaped: %s", out) + } + if !strings.Contains(out, `WITH_BACKSLASH="path\\to\\file"`) { + t.Fatalf("backslash not escaped: %s", out) + } + if !strings.Contains(out, `WITH_NEWLINE="line1\nline2"`) { + t.Fatalf("newline not escaped: %s", out) + } + if !strings.Contains(out, `WITH_ALL="say \"hi\"\nand \\go"`) { + t.Fatalf("combined escaping wrong: %s", out) + } +} + +func TestExportTOML_Escaping(t *testing.T) { + secrets := []KeyValue{ + {Key: "SIMPLE", Value: "hello"}, + {Key: "WITH_QUOTES", Value: `he said "hello"`}, + {Key: "WITH_BACKSLASH", Value: `path\to\file`}, + {Key: "WITH_BOTH", Value: `say "hi" and \ go`}, + } + out := captureStdout(t, func() { ExportTOML(secrets) }) + + if !strings.Contains(out, `SIMPLE = "hello"`) { + t.Fatalf("simple value wrong: %s", out) + } + if !strings.Contains(out, `WITH_QUOTES = "he said \"hello\""`) { + t.Fatalf("quoted value not escaped: %s", out) + } + if !strings.Contains(out, `WITH_BACKSLASH = "path\\to\\file"`) { + t.Fatalf("backslash not escaped: %s", out) + } + if !strings.Contains(out, `WITH_BOTH = "say \"hi\" and \\ go"`) { + t.Fatalf("combined escaping wrong: %s", out) + } +} + +func TestExportXML_KeyEscaping(t *testing.T) { + secrets := []KeyValue{ + {Key: `key"with"quotes`, Value: "val1"}, + {Key: "key&", Value: "val2"}, + {Key: "key", Value: "val3"}, + } + out := captureStdout(t, func() { ExportXML(secrets) }) + + var parsed xmlSecrets + if err := xml.Unmarshal([]byte(out), &parsed); err != nil { + t.Fatalf("xml with special key chars should parse, got: %v\noutput: %s", err, out) + } + got := map[string]string{} + for _, e := range parsed.Entries { + got[e.Name] = e.Value + } + for _, kv := range secrets { + if got[kv.Key] != kv.Value { + t.Fatalf("xml roundtrip for key %q: got %q want %q", kv.Key, got[kv.Key], kv.Value) + } + } +} + +func TestExportHCL_Escaping(t *testing.T) { + secrets := []KeyValue{ + {Key: "SIMPLE", Value: "hello"}, + {Key: "WITH_QUOTES", Value: `he said "hello"`}, + {Key: "WITH_BACKSLASH", Value: `path\to\file`}, + } + out := captureStdout(t, func() { ExportHCL(secrets) }) + + if !strings.Contains(out, `default = "hello"`) { + t.Fatalf("simple value wrong: %s", out) + } + if !strings.Contains(out, `default = "he said \"hello\""`) { + t.Fatalf("quoted value not escaped: %s", out) + } + if !strings.Contains(out, `default = "path\\to\\file"`) { + t.Fatalf("backslash not escaped: %s", out) + } +} + func TestExportINI_EscapesPercent(t *testing.T) { out := captureStdout(t, func() { ExportINI(sampleSecrets) }) if !strings.HasPrefix(out, "[DEFAULT]\n") {