Skip to content

Commit 6b8f6d6

Browse files
authored
fix: cap format string precision to prevent memory exhaustion (#1292)
* 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. * refactor: make maxPrecision configurable via StringsOption * test: verify high precision is allowed on older string lib versions * fix: move maxPrecision closer to usage and added comment
1 parent d942970 commit 6b8f6d6

5 files changed

Lines changed: 69 additions & 26 deletions

File tree

ext/formatting.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,9 @@ func (c *stringFormatter) Octal(arg ref.Val, locale string) (string, error) {
410410

411411
// stringFormatValidator implements the cel.ASTValidator interface allowing for static validation
412412
// of string.format calls.
413-
type stringFormatValidator struct{}
413+
type stringFormatValidator struct {
414+
maxPrecision int
415+
}
414416

415417
// Name returns the name of the validator.
416418
func (stringFormatValidator) Name() string {
@@ -427,7 +429,7 @@ func (stringFormatValidator) Configure(config cel.MutableValidatorConfig) error
427429

428430
// Validate parses all literal format strings and type checks the format clause against the argument
429431
// at the corresponding ordinal within the list literal argument to the function, if one is specified.
430-
func (stringFormatValidator) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) {
432+
func (v stringFormatValidator) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) {
431433
root := ast.NavigateAST(a)
432434
formatCallExprs := ast.MatchDescendants(root, matchConstantFormatStringWithListLiteralArgs(a))
433435
for _, e := range formatCallExprs {
@@ -439,7 +441,7 @@ func (stringFormatValidator) Validate(env *cel.Env, _ cel.ValidatorConfig, a *as
439441
ast: a,
440442
}
441443
// use a placeholder locale, since locale doesn't affect syntax
442-
_, err := parseFormatString(formatStr, formatCheck, formatCheck, "en_US")
444+
_, err := parseFormatString(formatStr, formatCheck, formatCheck, "en_US", v.maxPrecision)
443445
if err != nil {
444446
iss.ReportErrorAtID(getErrorExprID(e.ID(), err), "%v", err)
445447
continue
@@ -778,7 +780,7 @@ type formatListArgs interface {
778780

779781
// parseFormatString formats a string according to the string.format syntax, taking the clause implementations
780782
// from the provided FormatCallback and the args from the given FormatList.
781-
func parseFormatString(formatStr string, callback formatStringInterpolator, list formatListArgs, locale string) (string, error) {
783+
func parseFormatString(formatStr string, callback formatStringInterpolator, list formatListArgs, locale string, maxPrecision int) (string, error) {
782784
i := 0
783785
argIndex := 0
784786
var builtStr strings.Builder
@@ -802,7 +804,7 @@ func parseFormatString(formatStr string, callback formatStringInterpolator, list
802804
if int64(argIndex) >= list.Size() {
803805
return "", fmt.Errorf("index %d out of range", argIndex)
804806
}
805-
numRead, val, refErr := parseAndFormatClause(formatStr[i:], argAny, callback, list, locale)
807+
numRead, val, refErr := parseAndFormatClause(formatStr[i:], argAny, callback, list, locale, maxPrecision)
806808
if refErr != nil {
807809
return "", refErr
808810
}
@@ -826,9 +828,9 @@ func parseFormatString(formatStr string, callback formatStringInterpolator, list
826828

827829
// parseAndFormatClause parses the format clause at the start of the given string with val, and returns
828830
// how many characters were consumed and the substituted string form of val, or an error if one occurred.
829-
func parseAndFormatClause(formatStr string, val ref.Val, callback formatStringInterpolator, list formatListArgs, locale string) (int, string, error) {
831+
func parseAndFormatClause(formatStr string, val ref.Val, callback formatStringInterpolator, list formatListArgs, locale string, maxPrecision int) (int, string, error) {
830832
i := 1
831-
read, formatter, err := parseFormattingClause(formatStr[i:], callback)
833+
read, formatter, err := parseFormattingClause(formatStr[i:], callback, maxPrecision)
832834
i += read
833835
if err != nil {
834836
return -1, "", newParseFormatError("could not parse formatting clause", err)
@@ -841,9 +843,9 @@ func parseAndFormatClause(formatStr string, val ref.Val, callback formatStringIn
841843
return i, valStr, nil
842844
}
843845

844-
func parseFormattingClause(formatStr string, callback formatStringInterpolator) (int, clauseImpl, error) {
846+
func parseFormattingClause(formatStr string, callback formatStringInterpolator, maxPrecision int) (int, clauseImpl, error) {
845847
i := 0
846-
read, precision, err := parsePrecision(formatStr[i:])
848+
read, precision, err := parsePrecision(formatStr[i:], maxPrecision)
847849
i += read
848850
if err != nil {
849851
return -1, nil, fmt.Errorf("error while parsing precision: %w", err)
@@ -870,7 +872,7 @@ func parseFormattingClause(formatStr string, callback formatStringInterpolator)
870872
}
871873
}
872874

873-
func parsePrecision(formatStr string) (int, *int, error) {
875+
func parsePrecision(formatStr string, maxPrecision int) (int, *int, error) {
874876
i := 0
875877
if formatStr[i] != '.' {
876878
return i, nil, nil
@@ -891,6 +893,9 @@ func parsePrecision(formatStr string) (int, *int, error) {
891893
if err != nil {
892894
return -1, nil, fmt.Errorf("error while converting precision to integer: %w", err)
893895
}
896+
if maxPrecision > 0 && precision > maxPrecision {
897+
return -1, nil, fmt.Errorf("precision %d exceeds maximum allowed precision %d", precision, maxPrecision)
898+
}
894899
return i, &precision, nil
895900
}
896901

ext/formatting_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,14 @@ func TestStringFormat(t *testing.T) {
866866
formatArgs: "3.14",
867867
err: "error during formatting: octal clause can only be used on integers",
868868
},
869+
{
870+
name: "high precision allowed on older version",
871+
format: "%.200f",
872+
formatArgs: "1.0",
873+
expectedOutput: "1." + strings.Repeat("0", 200),
874+
skipCompileCheck: true,
875+
locale: "en_US",
876+
},
869877
}
870878
evalExpr := func(env *cel.Env, expr string, evalArgs any, expectedRuntimeCost uint64, expectedEstimatedCost checker.CostEstimate, t *testing.T) (ref.Val, error) {
871879
t.Logf("evaluating expr: %s", expr)

ext/formatting_v2.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,9 @@ func (c *stringFormatterV2) Octal(arg ref.Val) (string, error) {
402402

403403
// stringFormatValidatorV2 implements the cel.ASTValidator interface allowing for static validation
404404
// of string.format calls.
405-
type stringFormatValidatorV2 struct{}
405+
type stringFormatValidatorV2 struct {
406+
maxPrecision int
407+
}
406408

407409
// Name returns the name of the validator.
408410
func (stringFormatValidatorV2) Name() string {
@@ -419,7 +421,7 @@ func (stringFormatValidatorV2) Configure(config cel.MutableValidatorConfig) erro
419421

420422
// Validate parses all literal format strings and type checks the format clause against the argument
421423
// at the corresponding ordinal within the list literal argument to the function, if one is specified.
422-
func (stringFormatValidatorV2) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) {
424+
func (v stringFormatValidatorV2) Validate(env *cel.Env, _ cel.ValidatorConfig, a *ast.AST, iss *cel.Issues) {
423425
root := ast.NavigateAST(a)
424426
formatCallExprs := ast.MatchDescendants(root, matchConstantFormatStringWithListLiteralArgs(a))
425427
for _, e := range formatCallExprs {
@@ -431,7 +433,7 @@ func (stringFormatValidatorV2) Validate(env *cel.Env, _ cel.ValidatorConfig, a *
431433
ast: a,
432434
}
433435
// use a placeholder locale, since locale doesn't affect syntax
434-
_, err := parseFormatStringV2(formatStr, formatCheck, formatCheck)
436+
_, err := parseFormatStringV2(formatStr, formatCheck, formatCheck, v.maxPrecision)
435437
if err != nil {
436438
iss.ReportErrorAtID(getErrorExprID(e.ID(), err), "%v", err)
437439
continue
@@ -668,7 +670,7 @@ type formatStringInterpolatorV2 interface {
668670

669671
// parseFormatString formats a string according to the string.format syntax, taking the clause implementations
670672
// from the provided FormatCallback and the args from the given FormatList.
671-
func parseFormatStringV2(formatStr string, callback formatStringInterpolatorV2, list formatListArgs) (string, error) {
673+
func parseFormatStringV2(formatStr string, callback formatStringInterpolatorV2, list formatListArgs, maxPrecision int) (string, error) {
672674
i := 0
673675
argIndex := 0
674676
var builtStr strings.Builder
@@ -692,7 +694,7 @@ func parseFormatStringV2(formatStr string, callback formatStringInterpolatorV2,
692694
if int64(argIndex) >= list.Size() {
693695
return "", fmt.Errorf("index %d out of range", argIndex)
694696
}
695-
numRead, val, refErr := parseAndFormatClauseV2(formatStr[i:], argAny, callback, list)
697+
numRead, val, refErr := parseAndFormatClauseV2(formatStr[i:], argAny, callback, list, maxPrecision)
696698
if refErr != nil {
697699
return "", refErr
698700
}
@@ -716,9 +718,9 @@ func parseFormatStringV2(formatStr string, callback formatStringInterpolatorV2,
716718

717719
// parseAndFormatClause parses the format clause at the start of the given string with val, and returns
718720
// how many characters were consumed and the substituted string form of val, or an error if one occurred.
719-
func parseAndFormatClauseV2(formatStr string, val ref.Val, callback formatStringInterpolatorV2, list formatListArgs) (int, string, error) {
721+
func parseAndFormatClauseV2(formatStr string, val ref.Val, callback formatStringInterpolatorV2, list formatListArgs, maxPrecision int) (int, string, error) {
720722
i := 1
721-
read, formatter, err := parseFormattingClauseV2(formatStr[i:], callback)
723+
read, formatter, err := parseFormattingClauseV2(formatStr[i:], callback, maxPrecision)
722724
i += read
723725
if err != nil {
724726
return -1, "", newParseFormatError("could not parse formatting clause", err)
@@ -731,9 +733,9 @@ func parseAndFormatClauseV2(formatStr string, val ref.Val, callback formatString
731733
return i, valStr, nil
732734
}
733735

734-
func parseFormattingClauseV2(formatStr string, callback formatStringInterpolatorV2) (int, clauseImplV2, error) {
736+
func parseFormattingClauseV2(formatStr string, callback formatStringInterpolatorV2, maxPrecision int) (int, clauseImplV2, error) {
735737
i := 0
736-
read, precision, err := parsePrecisionV2(formatStr[i:])
738+
read, precision, err := parsePrecisionV2(formatStr[i:], maxPrecision)
737739
i += read
738740
if err != nil {
739741
return -1, nil, fmt.Errorf("error while parsing precision: %w", err)
@@ -760,7 +762,7 @@ func parseFormattingClauseV2(formatStr string, callback formatStringInterpolator
760762
}
761763
}
762764

763-
func parsePrecisionV2(formatStr string) (int, int, error) {
765+
func parsePrecisionV2(formatStr string, maxPrecision int) (int, int, error) {
764766
i := 0
765767
if formatStr[i] != '.' {
766768
return i, defaultPrecision, nil
@@ -784,5 +786,8 @@ func parsePrecisionV2(formatStr string) (int, int, error) {
784786
if precision < 0 {
785787
return -1, -1, fmt.Errorf("negative precision: %d", precision)
786788
}
789+
if maxPrecision > 0 && precision > maxPrecision {
790+
return -1, -1, fmt.Errorf("precision %d exceeds maximum allowed precision %d", precision, maxPrecision)
791+
}
787792
return i, precision, nil
788793
}

ext/formatting_v2_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,13 @@ func TestStringFormatV2(t *testing.T) {
880880
formatArgs: "3.14",
881881
err: "octal clause can only be used on ints and uints, was given double",
882882
},
883+
{
884+
name: "precision exceeds maximum",
885+
format: "%.9999999f",
886+
formatArgs: "3.14",
887+
skipCompileCheck: true,
888+
err: "precision 9999999 exceeds maximum allowed precision 100",
889+
},
883890
}
884891
evalExpr := func(env *cel.Env, expr string, evalArgs any, expectedRuntimeCost uint64, expectedEstimatedCost checker.CostEstimate, t *testing.T) (ref.Val, error) {
885892
t.Logf("evaluating expr: %s", expr)

ext/strings.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,9 @@ func Strings(options ...StringsOption) cel.EnvOption {
303303
}
304304

305305
type stringLib struct {
306-
locale string
307-
version uint32
306+
locale string
307+
version uint32
308+
maxPrecision int
308309
}
309310

310311
// LibraryName implements the SingletonLibrary interface method.
@@ -353,6 +354,16 @@ func StringsValidateFormatCalls(value bool) StringsOption {
353354
}
354355
}
355356

357+
// StringsMaxPrecision configures the maximum precision for floating-point format clauses.
358+
//
359+
// If not set, the default is 100 for version >= 5, and no limit for earlier versions.
360+
func StringsMaxPrecision(limit int) StringsOption {
361+
return func(lib *stringLib) *stringLib {
362+
lib.maxPrecision = limit
363+
return lib
364+
}
365+
}
366+
356367
// CompileOptions implements the Library interface method.
357368
func (lib *stringLib) CompileOptions() []cel.EnvOption {
358369
formatLocale := "en_US"
@@ -470,22 +481,29 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption {
470481
return stringOrError(upperASCII(string(s)))
471482
}))),
472483
}
484+
// maxPrecision is unbounded (0) for versions < 5 to maintain backward
485+
// compatibility. For version >= 5, the default is 100 if not explicitly
486+
// configured via StringsMaxPrecision().
487+
maxPrecision := lib.maxPrecision
488+
if maxPrecision == 0 && lib.version >= 5 {
489+
maxPrecision = 100
490+
}
473491
if lib.version >= 1 {
474492
if lib.version >= 4 {
475493
opts = append(opts, cel.Function("format",
476494
cel.MemberOverload("string_format", []*cel.Type{cel.StringType, cel.ListType(cel.DynType)}, cel.StringType,
477495
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
478496
s := string(args[0].(types.String))
479497
formatArgs := args[1].(traits.Lister)
480-
return stringOrError(parseFormatStringV2(s, &stringFormatterV2{}, &stringArgList{formatArgs}))
498+
return stringOrError(parseFormatStringV2(s, &stringFormatterV2{}, &stringArgList{formatArgs}, maxPrecision))
481499
}))))
482500
} else {
483501
opts = append(opts, cel.Function("format",
484502
cel.MemberOverload("string_format", []*cel.Type{cel.StringType, cel.ListType(cel.DynType)}, cel.StringType,
485503
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
486504
s := string(args[0].(types.String))
487505
formatArgs := args[1].(traits.Lister)
488-
return stringOrError(parseFormatString(s, &stringFormatter{}, &stringArgList{formatArgs}, formatLocale))
506+
return stringOrError(parseFormatString(s, &stringFormatter{}, &stringArgList{formatArgs}, formatLocale, maxPrecision))
489507
}))))
490508
}
491509
opts = append(opts,
@@ -544,9 +562,9 @@ func (lib *stringLib) CompileOptions() []cel.EnvOption {
544562
}
545563
if lib.version >= 1 {
546564
if lib.version >= 4 {
547-
opts = append(opts, cel.ASTValidators(stringFormatValidatorV2{}))
565+
opts = append(opts, cel.ASTValidators(stringFormatValidatorV2{maxPrecision: maxPrecision}))
548566
} else {
549-
opts = append(opts, cel.ASTValidators(stringFormatValidator{}))
567+
opts = append(opts, cel.ASTValidators(stringFormatValidator{maxPrecision: maxPrecision}))
550568
}
551569
}
552570
return opts

0 commit comments

Comments
 (0)