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, "fromJson": fromJSON,
"fromJsonArray": fromJSONArray, "fromJsonArray": fromJSONArray,
"gzip": gzipFunc, "gzip": gzipFunc,
"ungzip": ungzipFunc, "gunzip": gunzipFunc,
// Duration helpers // Duration helpers
"mustToDuration": mustToDuration, "mustToDuration": mustToDuration,
@ -487,13 +487,17 @@ func durationTruncateTo(v, m any) time.Duration {
// //
// This is designed to be called from a template. // This is designed to be called from a template.
func gzipFunc(str string) (string, error) { 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 var b bytes.Buffer
w := gzip.NewWriter(&b) w := gzip.NewWriter(&b)
if _, err := w.Write([]byte(str)); err != nil { if _, err := w.Write([]byte(str)); err != nil {
return "", err return "", fmt.Errorf("gzip: write failed: %w", err)
} }
if err := w.Close(); err != nil { if err := w.Close(); err != nil {
return "", err return "", fmt.Errorf("gzip: close failed: %w", err)
} }
encoded := base64.StdEncoding.EncodeToString(b.Bytes()) encoded := base64.StdEncoding.EncodeToString(b.Bytes())
@ -507,27 +511,27 @@ func gzipFunc(str string) (string, error) {
return encoded, nil 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. // It enforces a size limit (1MB) on the input and output to prevent abuse.
// //
// This is designed to be called from a template. // 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. // Kubernetes limit for Secret/ConfigMap is 1MB.
const maxLimit = 1048576 const maxLimit = 1048576
if len(str) > maxLimit { 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) decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil { 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)) r, err := gzip.NewReader(bytes.NewReader(decoded))
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("gunzip: gzip reader failed: %w", err)
} }
defer r.Close() defer r.Close()
@ -536,16 +540,16 @@ func ungzipFunc(str string) (string, error) {
b, err := io.ReadAll(limitR) b, err := io.ReadAll(limitR)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("gunzip: read failed: %w", err)
} }
if len(b) > maxLimit { 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. // Ensure the content is valid text (UTF-8) to prevent binary obfuscation.
if !utf8.Valid(b) { 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 return string(b), nil

@ -138,7 +138,7 @@ keyInElement1 = "valueInElement1"`,
expect: `H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA=`, expect: `H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA=`,
vars: nil, vars: nil,
}, { }, {
tpl: `{{ "H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA=" | ungzip }}`, tpl: `{{ "H4sIAAAAAAAA/8pIzcnJVyjPL8pJAQQAAP//hRFKDQsAAAA=" | gunzip }}`,
expect: `hello world`, expect: `hello world`,
vars: nil, vars: nil,
}} }}
@ -505,10 +505,10 @@ func TestMerge(t *testing.T) {
assert.Equal(t, expected, dict["dst"]) 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 // correctly mitigates decompression bomb (zip bomb) attacks by strictly
// enforcing a 1MB limit on the decompressed output. // enforcing a 1MB limit on the decompressed output.
func TestUngzipDecompressionBomb(t *testing.T) { func TestGunzipDecompressionBomb(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
w := gzip.NewWriter(&buf) w := gzip.NewWriter(&buf)
@ -522,11 +522,31 @@ func TestUngzipDecompressionBomb(t *testing.T) {
t.Fatalf("failed to close gzip: %v", err) 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()) encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
// Attempting to ungzip should result in an error since it exceeds 1MB limit // Attempting to gunzip should result in an error since it exceeds 1MB limit
_, err := ungzipFunc(encoded) _, err := gunzipFunc(encoded)
require.Error(t, err) 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