From 7d5bcac49b87362fb431a0956eccebbe4e74d367 Mon Sep 17 00:00:00 2001 From: "piotr.laczykowski" Date: Wed, 25 Mar 2026 15:14:04 +0100 Subject: [PATCH] fix(engine): override toPrettyJson to disable HTML escaping Sprig's toPrettyJson uses json.MarshalIndent which HTML-escapes &, <, > to \u0026, \u003c, \u003e. This breaks URLs and other strings containing these characters when used in Helm chart templates. Override toPrettyJson and mustToPrettyJson in the engine FuncMap with implementations that use json.Encoder with SetEscapeHTML(false), matching Sprig's two-space indentation for backward compatibility. Signed-off-by: piotr.laczykowski --- pkg/engine/funcs.go | 68 +++++++++++++++++++++++++++++++++------- pkg/engine/funcs_test.go | 20 ++++++++++++ 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index e03c13b38..b5fb62578 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -49,18 +49,20 @@ func funcMap() template.FuncMap { // Add some extra functionality extra := template.FuncMap{ - "toToml": toTOML, - "mustToToml": mustToTOML, - "fromToml": fromTOML, - "toYaml": toYAML, - "mustToYaml": mustToYAML, - "toYamlPretty": toYAMLPretty, - "fromYaml": fromYAML, - "fromYamlArray": fromYAMLArray, - "toJson": toJSON, - "mustToJson": mustToJSON, - "fromJson": fromJSON, - "fromJsonArray": fromJSONArray, + "toToml": toTOML, + "mustToToml": mustToTOML, + "fromToml": fromTOML, + "toYaml": toYAML, + "mustToYaml": mustToYAML, + "toYamlPretty": toYAMLPretty, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, + "toJson": toJSON, + "mustToJson": mustToJSON, + "toPrettyJson": toPrettyJSON, + "mustToPrettyJson": mustToPrettyJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, // This is a placeholder for the "include" function, which is // late-bound to a template. By declaring it here, we preserve the @@ -220,6 +222,48 @@ func mustToJSON(v any) string { return string(data) } +// encodePrettyJSON encodes v as indented JSON without HTML-escaping special +// characters (&, <, >). It uses two-space indentation to match the behavior +// of Sprig's toPrettyJson. +func encodePrettyJSON(v any) (string, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + if err := enc.Encode(v); err != nil { + return "", err + } + return strings.TrimSuffix(buf.String(), "\n"), nil +} + +// toPrettyJSON takes an interface, marshals it to indented JSON without +// HTML-escaping special characters (&, <, >), and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toPrettyJSON(v any) string { + s, err := encodePrettyJSON(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return s +} + +// mustToPrettyJSON takes an interface, marshals it to indented JSON without +// HTML-escaping special characters (&, <, >), and returns a string. +// It will panic if there is an error. +// +// This is designed to be called from a template when you need to ensure that +// the output JSON is valid. +func mustToPrettyJSON(v any) string { + s, err := encodePrettyJSON(v) + if err != nil { + panic(err) + } + return s +} + // fromJSON converts a JSON document into a map[string]interface{}. // // This is not a general-purpose JSON parser, and will not parse all valid diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index be9d0153f..824f1033c 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -69,6 +69,15 @@ keyInElement1 = "valueInElement1"`, tpl: `{{ toJson . }}`, expect: `{"foo":"bar"}`, vars: map[string]any{"foo": "bar"}, + }, { + // toPrettyJson must not HTML-escape &, <, > and must use 2-space indent + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"url\": \"https://example.com?a=1&b=2<>\"\n}", + vars: map[string]any{"url": "https://example.com?a=1&b=2<>"}, + }, { + tpl: "{{ toPrettyJson . }}", + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, }, { tpl: `{{ fromYaml . }}`, expect: "map[hello:world]", @@ -151,6 +160,17 @@ keyInElement1 = "valueInElement1"`, }, { tpl: `{{ mustToJson . }}`, vars: loopMap, + }, { + tpl: `{{ mustToPrettyJson . }}`, + vars: loopMap, // circular reference must panic + }, { + tpl: `{{ mustToPrettyJson . }}`, + expect: "{\n \"foo\": \"bar\"\n}", + vars: map[string]any{"foo": "bar"}, + }, { + tpl: `{{ toPrettyJson . }}`, + expect: "", // circular reference must swallow error and return "" + vars: loopMap, }, { tpl: `{{ toYaml . }}`, expect: "", // should return empty string and swallow error