diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 5ef83df2e..b6a87049d 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -72,7 +72,7 @@ func funcMap() template.FuncMap { "fromJson": fromJSON, "fromJsonArray": fromJSONArray, "gzip": gzipFunc, - "ungzip": ungzipFunc, + "gunzip": gunzipFunc, // Duration helpers "mustToDuration": mustToDuration, @@ -487,13 +487,17 @@ func durationTruncateTo(v, m any) time.Duration { // // This is designed to be called from a template. func gzipFunc(str string) (string, error) { + if !utf8.ValidString(str) { + return "", errors.New("gzip: content is not valid UTF-8 text") + } + var b bytes.Buffer w := gzip.NewWriter(&b) if _, err := w.Write([]byte(str)); err != nil { - return "", err + return "", fmt.Errorf("gzip: write failed: %w", err) } if err := w.Close(); err != nil { - return "", err + return "", fmt.Errorf("gzip: close failed: %w", err) } encoded := base64.StdEncoding.EncodeToString(b.Bytes()) @@ -507,27 +511,27 @@ func gzipFunc(str string) (string, error) { return encoded, nil } -// ungzipFunc decodes a base64 encoded and gzip-compressed string. +// gunzipFunc decodes a base64 encoded and gzip-compressed string. // // It enforces a size limit (1MB) on the input and output to prevent abuse. // // This is designed to be called from a template. -func ungzipFunc(str string) (string, error) { +func gunzipFunc(str string) (string, error) { // Kubernetes limit for Secret/ConfigMap is 1MB. const maxLimit = 1048576 if len(str) > maxLimit { - return "", fmt.Errorf("ungzip: input size %d exceeds limit of %d bytes", len(str), maxLimit) + return "", fmt.Errorf("gunzip: input size %d exceeds limit of %d bytes", len(str), maxLimit) } decoded, err := base64.StdEncoding.DecodeString(str) if err != nil { - return "", fmt.Errorf("ungzip: base64 decode failed: %w", err) + return "", fmt.Errorf("gunzip: base64 decode failed: %w", err) } r, err := gzip.NewReader(bytes.NewReader(decoded)) if err != nil { - return "", err + return "", fmt.Errorf("gunzip: gzip reader failed: %w", err) } defer r.Close() @@ -536,16 +540,16 @@ func ungzipFunc(str string) (string, error) { b, err := io.ReadAll(limitR) if err != nil { - return "", err + return "", fmt.Errorf("gunzip: read failed: %w", err) } if len(b) > maxLimit { - return "", fmt.Errorf("ungzip: decompressed content exceeds size limit of %d bytes", maxLimit) + return "", fmt.Errorf("gunzip: decompressed content exceeds size limit of %d bytes", maxLimit) } // Ensure the content is valid text (UTF-8) to prevent binary obfuscation. if !utf8.Valid(b) { - return "", errors.New("ungzip: content is not valid UTF-8 text") + return "", errors.New("gunzip: content is not valid UTF-8 text") } return string(b), nil diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 78a10e7c6..6d256b49c 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -138,7 +138,7 @@ keyInElement1 = "valueInElement1"`, expect: `H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA=`, vars: nil, }, { - tpl: `{{ "H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA=" | ungzip }}`, + tpl: `{{ "H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA=" | gunzip }}`, expect: `hello world`, vars: nil, }} @@ -505,10 +505,10 @@ func TestMerge(t *testing.T) { assert.Equal(t, expected, dict["dst"]) } -// TestUngzipDecompressionBomb verifies that the ungzip template function +// TestGunzipDecompressionBomb verifies that the gunzip template function // correctly mitigates decompression bomb (zip bomb) attacks by strictly // enforcing a 1MB limit on the decompressed output. -func TestUngzipDecompressionBomb(t *testing.T) { +func TestGunzipDecompressionBomb(t *testing.T) { var buf bytes.Buffer w := gzip.NewWriter(&buf) @@ -522,11 +522,31 @@ func TestUngzipDecompressionBomb(t *testing.T) { t.Fatalf("failed to close gzip: %v", err) } - // Base64 encode the compressed bomb as expected by ungzipFunc + // Base64 encode the compressed bomb as expected by gunzipFunc encoded := base64.StdEncoding.EncodeToString(buf.Bytes()) - // Attempting to ungzip should result in an error since it exceeds 1MB limit - _, err := ungzipFunc(encoded) + // Attempting to gunzip should result in an error since it exceeds 1MB limit + _, err := gunzipFunc(encoded) require.Error(t, err) - assert.Contains(t, err.Error(), "decompressed content exceeds size limit of 1048576 bytes") + assert.Contains(t, err.Error(), "gunzip: decompressed content exceeds size limit of 1048576 bytes") +} + +func TestGzipGunzipUTF8Validation(t *testing.T) { + // gzipFunc rejects invalid UTF-8 string + invalidUTF8 := string([]byte{0xff, 0xfe, 0xfd}) + _, err := gzipFunc(invalidUTF8) + require.Error(t, err) + assert.Contains(t, err.Error(), "gzip: content is not valid UTF-8 text") + + // gunzipFunc rejects decompressed content that is not valid UTF-8 + var buf bytes.Buffer + w := gzip.NewWriter(&buf) + _, err = w.Write([]byte{0xff, 0xfe, 0xfd}) + require.NoError(t, err) + require.NoError(t, w.Close()) + + encoded := base64.StdEncoding.EncodeToString(buf.Bytes()) + _, err = gunzipFunc(encoded) + require.Error(t, err) + assert.Contains(t, err.Error(), "gunzip: content is not valid UTF-8 text") }