feat/add-gzip-functions: rename ungzip to gunzip, validate utf8 and wrap errors

Signed-off-by: owan <owan.io1992@gmail.com>
pull/31746/head
owan 3 days ago
parent 5a09a7c227
commit 268e5ce40d

@ -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

@ -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")
}

Loading…
Cancel
Save