From 575f267bcd7a594dc39c0e58e567e3c38e442f42 Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 23 Mar 2026 09:16:02 +0100 Subject: [PATCH 1/4] fix: cap format string precision to prevent memory exhaustion parsePrecision() and parsePrecisionV2() accept unbounded precision values. An expression like "%.9999999f".format([3.14]) allocates 792MB at cost 501. Add maxPrecision=1000 cap. --- ext/formatting.go | 5 +++++ ext/formatting_v2.go | 3 +++ 2 files changed, 8 insertions(+) diff --git a/ext/formatting.go b/ext/formatting.go index 111184b73..40aefa582 100644 --- a/ext/formatting.go +++ b/ext/formatting.go @@ -870,6 +870,8 @@ func parseFormattingClause(formatStr string, callback formatStringInterpolator) } } +const maxPrecision = 1000 + func parsePrecision(formatStr string) (int, *int, error) { i := 0 if formatStr[i] != '.' { @@ -891,6 +893,9 @@ func parsePrecision(formatStr string) (int, *int, error) { if err != nil { return -1, nil, fmt.Errorf("error while converting precision to integer: %w", err) } + if precision > maxPrecision { + return -1, nil, fmt.Errorf("precision %d exceeds maximum allowed precision %d", precision, maxPrecision) + } return i, &precision, nil } diff --git a/ext/formatting_v2.go b/ext/formatting_v2.go index 6ac55b5d9..46cf228fb 100644 --- a/ext/formatting_v2.go +++ b/ext/formatting_v2.go @@ -784,5 +784,8 @@ func parsePrecisionV2(formatStr string) (int, int, error) { if precision < 0 { return -1, -1, fmt.Errorf("negative precision: %d", precision) } + if precision > maxPrecision { + return -1, -1, fmt.Errorf("precision %d exceeds maximum allowed precision %d", precision, maxPrecision) + } return i, precision, nil } From 166120cba45532d2a166f1b92115f4c9d52b8da3 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 24 Mar 2026 10:44:46 +0100 Subject: [PATCH 2/4] refactor: make maxPrecision configurable via StringsOption --- ext/formatting.go | 26 +++++++++++++------------- ext/formatting_v2.go | 24 +++++++++++++----------- ext/formatting_v2_test.go | 7 +++++++ ext/strings.go | 27 +++++++++++++++++++++------ 4 files changed, 54 insertions(+), 30 deletions(-) diff --git a/ext/formatting.go b/ext/formatting.go index 40aefa582..35fb17048 100644 --- a/ext/formatting.go +++ b/ext/formatting.go @@ -410,7 +410,9 @@ func (c *stringFormatter) Octal(arg ref.Val, locale string) (string, error) { // stringFormatValidator implements the cel.ASTValidator interface allowing for static validation // of string.format calls. -type stringFormatValidator struct{} +type stringFormatValidator struct { + maxPrecision int +} // Name returns the name of the validator. func (stringFormatValidator) Name() string { @@ -427,7 +429,7 @@ func (stringFormatValidator) Configure(config cel.MutableValidatorConfig) error // Validate parses all literal format strings and type checks the format clause against the argument // at the corresponding ordinal within the list literal argument to the function, if one is specified. -func (stringFormatValidator) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) { +func (v stringFormatValidator) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) { root := ast.NavigateAST(a) formatCallExprs := ast.MatchDescendants(root, matchConstantFormatStringWithListLiteralArgs(a)) for _, e := range formatCallExprs { @@ -439,7 +441,7 @@ func (stringFormatValidator) Validate(env *cel.Env, _ cel.ValidatorConfig, a *as ast: a, } // use a placeholder locale, since locale doesn't affect syntax - _, err := parseFormatString(formatStr, formatCheck, formatCheck, "en_US") + _, err := parseFormatString(formatStr, formatCheck, formatCheck, "en_US", v.maxPrecision) if err != nil { iss.ReportErrorAtID(getErrorExprID(e.ID(), err), "%v", err) continue @@ -778,7 +780,7 @@ type formatListArgs interface { // parseFormatString formats a string according to the string.format syntax, taking the clause implementations // from the provided FormatCallback and the args from the given FormatList. -func parseFormatString(formatStr string, callback formatStringInterpolator, list formatListArgs, locale string) (string, error) { +func parseFormatString(formatStr string, callback formatStringInterpolator, list formatListArgs, locale string, maxPrecision int) (string, error) { i := 0 argIndex := 0 var builtStr strings.Builder @@ -802,7 +804,7 @@ func parseFormatString(formatStr string, callback formatStringInterpolator, list if int64(argIndex) >= list.Size() { return "", fmt.Errorf("index %d out of range", argIndex) } - numRead, val, refErr := parseAndFormatClause(formatStr[i:], argAny, callback, list, locale) + numRead, val, refErr := parseAndFormatClause(formatStr[i:], argAny, callback, list, locale, maxPrecision) if refErr != nil { return "", refErr } @@ -826,9 +828,9 @@ func parseFormatString(formatStr string, callback formatStringInterpolator, list // parseAndFormatClause parses the format clause at the start of the given string with val, and returns // how many characters were consumed and the substituted string form of val, or an error if one occurred. -func parseAndFormatClause(formatStr string, val ref.Val, callback formatStringInterpolator, list formatListArgs, locale string) (int, string, error) { +func parseAndFormatClause(formatStr string, val ref.Val, callback formatStringInterpolator, list formatListArgs, locale string, maxPrecision int) (int, string, error) { i := 1 - read, formatter, err := parseFormattingClause(formatStr[i:], callback) + read, formatter, err := parseFormattingClause(formatStr[i:], callback, maxPrecision) i += read if err != nil { return -1, "", newParseFormatError("could not parse formatting clause", err) @@ -841,9 +843,9 @@ func parseAndFormatClause(formatStr string, val ref.Val, callback formatStringIn return i, valStr, nil } -func parseFormattingClause(formatStr string, callback formatStringInterpolator) (int, clauseImpl, error) { +func parseFormattingClause(formatStr string, callback formatStringInterpolator, maxPrecision int) (int, clauseImpl, error) { i := 0 - read, precision, err := parsePrecision(formatStr[i:]) + read, precision, err := parsePrecision(formatStr[i:], maxPrecision) i += read if err != nil { return -1, nil, fmt.Errorf("error while parsing precision: %w", err) @@ -870,9 +872,7 @@ func parseFormattingClause(formatStr string, callback formatStringInterpolator) } } -const maxPrecision = 1000 - -func parsePrecision(formatStr string) (int, *int, error) { +func parsePrecision(formatStr string, maxPrecision int) (int, *int, error) { i := 0 if formatStr[i] != '.' { return i, nil, nil @@ -893,7 +893,7 @@ func parsePrecision(formatStr string) (int, *int, error) { if err != nil { return -1, nil, fmt.Errorf("error while converting precision to integer: %w", err) } - if precision > maxPrecision { + if maxPrecision > 0 && precision > maxPrecision { return -1, nil, fmt.Errorf("precision %d exceeds maximum allowed precision %d", precision, maxPrecision) } return i, &precision, nil diff --git a/ext/formatting_v2.go b/ext/formatting_v2.go index 46cf228fb..f923cc7e1 100644 --- a/ext/formatting_v2.go +++ b/ext/formatting_v2.go @@ -402,7 +402,9 @@ func (c *stringFormatterV2) Octal(arg ref.Val) (string, error) { // stringFormatValidatorV2 implements the cel.ASTValidator interface allowing for static validation // of string.format calls. -type stringFormatValidatorV2 struct{} +type stringFormatValidatorV2 struct { + maxPrecision int +} // Name returns the name of the validator. func (stringFormatValidatorV2) Name() string { @@ -419,7 +421,7 @@ func (stringFormatValidatorV2) Configure(config cel.MutableValidatorConfig) erro // Validate parses all literal format strings and type checks the format clause against the argument // at the corresponding ordinal within the list literal argument to the function, if one is specified. -func (stringFormatValidatorV2) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) { +func (v stringFormatValidatorV2) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) { root := ast.NavigateAST(a) formatCallExprs := ast.MatchDescendants(root, matchConstantFormatStringWithListLiteralArgs(a)) for _, e := range formatCallExprs { @@ -431,7 +433,7 @@ func (stringFormatValidatorV2) Validate(env *cel.Env, _ cel.ValidatorConfig, a * ast: a, } // use a placeholder locale, since locale doesn't affect syntax - _, err := parseFormatStringV2(formatStr, formatCheck, formatCheck) + _, err := parseFormatStringV2(formatStr, formatCheck, formatCheck, v.maxPrecision) if err != nil { iss.ReportErrorAtID(getErrorExprID(e.ID(), err), "%v", err) continue @@ -668,7 +670,7 @@ type formatStringInterpolatorV2 interface { // parseFormatString formats a string according to the string.format syntax, taking the clause implementations // from the provided FormatCallback and the args from the given FormatList. -func parseFormatStringV2(formatStr string, callback formatStringInterpolatorV2, list formatListArgs) (string, error) { +func parseFormatStringV2(formatStr string, callback formatStringInterpolatorV2, list formatListArgs, maxPrecision int) (string, error) { i := 0 argIndex := 0 var builtStr strings.Builder @@ -692,7 +694,7 @@ func parseFormatStringV2(formatStr string, callback formatStringInterpolatorV2, if int64(argIndex) >= list.Size() { return "", fmt.Errorf("index %d out of range", argIndex) } - numRead, val, refErr := parseAndFormatClauseV2(formatStr[i:], argAny, callback, list) + numRead, val, refErr := parseAndFormatClauseV2(formatStr[i:], argAny, callback, list, maxPrecision) if refErr != nil { return "", refErr } @@ -716,9 +718,9 @@ func parseFormatStringV2(formatStr string, callback formatStringInterpolatorV2, // parseAndFormatClause parses the format clause at the start of the given string with val, and returns // how many characters were consumed and the substituted string form of val, or an error if one occurred. -func parseAndFormatClauseV2(formatStr string, val ref.Val, callback formatStringInterpolatorV2, list formatListArgs) (int, string, error) { +func parseAndFormatClauseV2(formatStr string, val ref.Val, callback formatStringInterpolatorV2, list formatListArgs, maxPrecision int) (int, string, error) { i := 1 - read, formatter, err := parseFormattingClauseV2(formatStr[i:], callback) + read, formatter, err := parseFormattingClauseV2(formatStr[i:], callback, maxPrecision) i += read if err != nil { return -1, "", newParseFormatError("could not parse formatting clause", err) @@ -731,9 +733,9 @@ func parseAndFormatClauseV2(formatStr string, val ref.Val, callback formatString return i, valStr, nil } -func parseFormattingClauseV2(formatStr string, callback formatStringInterpolatorV2) (int, clauseImplV2, error) { +func parseFormattingClauseV2(formatStr string, callback formatStringInterpolatorV2, maxPrecision int) (int, clauseImplV2, error) { i := 0 - read, precision, err := parsePrecisionV2(formatStr[i:]) + read, precision, err := parsePrecisionV2(formatStr[i:], maxPrecision) i += read if err != nil { return -1, nil, fmt.Errorf("error while parsing precision: %w", err) @@ -760,7 +762,7 @@ func parseFormattingClauseV2(formatStr string, callback formatStringInterpolator } } -func parsePrecisionV2(formatStr string) (int, int, error) { +func parsePrecisionV2(formatStr string, maxPrecision int) (int, int, error) { i := 0 if formatStr[i] != '.' { return i, defaultPrecision, nil @@ -784,7 +786,7 @@ func parsePrecisionV2(formatStr string) (int, int, error) { if precision < 0 { return -1, -1, fmt.Errorf("negative precision: %d", precision) } - if precision > maxPrecision { + if maxPrecision > 0 && precision > maxPrecision { return -1, -1, fmt.Errorf("precision %d exceeds maximum allowed precision %d", precision, maxPrecision) } return i, precision, nil diff --git a/ext/formatting_v2_test.go b/ext/formatting_v2_test.go index 46aeb688e..a183dba38 100644 --- a/ext/formatting_v2_test.go +++ b/ext/formatting_v2_test.go @@ -880,6 +880,13 @@ func TestStringFormatV2(t *testing.T) { formatArgs: "3.14", err: "octal clause can only be used on ints and uints, was given double", }, + { + name: "precision exceeds maximum", + format: "%.9999999f", + formatArgs: "3.14", + skipCompileCheck: true, + err: "precision 9999999 exceeds maximum allowed precision 100", + }, } evalExpr := func(env *cel.Env, expr string, evalArgs any, expectedRuntimeCost uint64, expectedEstimatedCost checker.CostEstimate, t *testing.T) (ref.Val, error) { t.Logf("evaluating expr: %s", expr) diff --git a/ext/strings.go b/ext/strings.go index de65421f6..52a19de74 100644 --- a/ext/strings.go +++ b/ext/strings.go @@ -303,8 +303,9 @@ func Strings(options ...StringsOption) cel.EnvOption { } type stringLib struct { - locale string - version uint32 + locale string + version uint32 + maxPrecision int } // LibraryName implements the SingletonLibrary interface method. @@ -353,8 +354,22 @@ func StringsValidateFormatCalls(value bool) StringsOption { } } +// StringsMaxPrecision configures the maximum precision for floating-point format clauses. +// +// If not set, the default is 100 for version >= 5, and no limit for earlier versions. +func StringsMaxPrecision(limit int) StringsOption { + return func(lib *stringLib) *stringLib { + lib.maxPrecision = limit + return lib + } +} + // CompileOptions implements the Library interface method. func (lib *stringLib) CompileOptions() []cel.EnvOption { + maxPrecision := lib.maxPrecision + if maxPrecision == 0 && lib.version >= 5 { + maxPrecision = 100 + } formatLocale := "en_US" if lib.version < 4 && lib.locale != "" { // ensure locale is properly-formed if set @@ -477,7 +492,7 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption { cel.FunctionBinding(func(args ...ref.Val) ref.Val { s := string(args[0].(types.String)) formatArgs := args[1].(traits.Lister) - return stringOrError(parseFormatStringV2(s, &stringFormatterV2{}, &stringArgList{formatArgs})) + return stringOrError(parseFormatStringV2(s, &stringFormatterV2{}, &stringArgList{formatArgs}, maxPrecision)) })))) } else { opts = append(opts, cel.Function("format", @@ -485,7 +500,7 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption { cel.FunctionBinding(func(args ...ref.Val) ref.Val { s := string(args[0].(types.String)) formatArgs := args[1].(traits.Lister) - return stringOrError(parseFormatString(s, &stringFormatter{}, &stringArgList{formatArgs}, formatLocale)) + return stringOrError(parseFormatString(s, &stringFormatter{}, &stringArgList{formatArgs}, formatLocale, maxPrecision)) })))) } opts = append(opts, @@ -544,9 +559,9 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption { } if lib.version >= 1 { if lib.version >= 4 { - opts = append(opts, cel.ASTValidators(stringFormatValidatorV2{})) + opts = append(opts, cel.ASTValidators(stringFormatValidatorV2{maxPrecision: maxPrecision})) } else { - opts = append(opts, cel.ASTValidators(stringFormatValidator{})) + opts = append(opts, cel.ASTValidators(stringFormatValidator{maxPrecision: maxPrecision})) } } return opts From c3412171120a4204bbf90ca1c007ac1f637c2d00 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 24 Mar 2026 10:52:55 +0100 Subject: [PATCH 3/4] test: verify high precision is allowed on older string lib versions --- ext/formatting_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ext/formatting_test.go b/ext/formatting_test.go index 6b1f25066..e77e3a936 100644 --- a/ext/formatting_test.go +++ b/ext/formatting_test.go @@ -866,6 +866,14 @@ func TestStringFormat(t *testing.T) { formatArgs: "3.14", err: "error during formatting: octal clause can only be used on integers", }, + { + name: "high precision allowed on older version", + format: "%.200f", + formatArgs: "1.0", + expectedOutput: "1." + strings.Repeat("0", 200), + skipCompileCheck: true, + locale: "en_US", + }, } evalExpr := func(env *cel.Env, expr string, evalArgs any, expectedRuntimeCost uint64, expectedEstimatedCost checker.CostEstimate, t *testing.T) (ref.Val, error) { t.Logf("evaluating expr: %s", expr) From 4b9f37c3ca297833dddb9f86d698a4eb5dda51d8 Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 31 Mar 2026 09:12:06 +0200 Subject: [PATCH 4/4] fix: move maxPrecision closer to usage and added comment --- ext/strings.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ext/strings.go b/ext/strings.go index 52a19de74..66b7806a3 100644 --- a/ext/strings.go +++ b/ext/strings.go @@ -366,10 +366,6 @@ func StringsMaxPrecision(limit int) StringsOption { // CompileOptions implements the Library interface method. func (lib *stringLib) CompileOptions() []cel.EnvOption { - maxPrecision := lib.maxPrecision - if maxPrecision == 0 && lib.version >= 5 { - maxPrecision = 100 - } formatLocale := "en_US" if lib.version < 4 && lib.locale != "" { // ensure locale is properly-formed if set @@ -485,6 +481,13 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption { return stringOrError(upperASCII(string(s))) }))), } + // maxPrecision is unbounded (0) for versions < 5 to maintain backward + // compatibility. For version >= 5, the default is 100 if not explicitly + // configured via StringsMaxPrecision(). + maxPrecision := lib.maxPrecision + if maxPrecision == 0 && lib.version >= 5 { + maxPrecision = 100 + } if lib.version >= 1 { if lib.version >= 4 { opts = append(opts, cel.Function("format",