From 9b5a353f21668b9cc12ead64c773d315092b1d98 Mon Sep 17 00:00:00 2001 From: "Ali Firas (thesmartshadow)" Date: Sat, 11 Apr 2026 19:59:59 +0000 Subject: [PATCH] fix: enforce expression size limit before source construction Enforce the configured ParserExpressionSizeLimit in Env.Compile() and Env.Parse() before calling common.NewTextSource(), preventing memory allocation proportional to the full input size for oversized expressions. Previously, oversized expressions were correctly rejected but only after the internal rune buffer had already been allocated. This allowed substantial memory allocation even when a strict size limit was configured. The fix adds an early utf8.RuneCountInString() check which is O(n) in CPU but avoids the additional memory allocation and GC pressure caused by eager source/rune-buffer construction. CWE-400: Uncontrolled Resource Consumption --- cel/cel_test.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ cel/env.go | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/cel/cel_test.go b/cel/cel_test.go index 9dd3fd869..c7d0bd6be 100644 --- a/cel/cel_test.go +++ b/cel/cel_test.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "reflect" + "runtime" "strings" "sync" "testing" @@ -3694,3 +3695,55 @@ func interpret(t testing.TB, env *Env, expr string, vars any) (ref.Val, error) { } return out, nil } + +func TestExpressionSizeLimitEarlyEnforcement(t *testing.T) { + env, err := NewEnv(ParserExpressionSizeLimit(1000)) + if err != nil { + t.Fatalf("NewEnv() failed: %v", err) + } + + tests := []struct { + name string + mode string + }{ + {name: "compile_rejects_oversized", mode: "compile"}, + {name: "parse_rejects_oversized", mode: "parse"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + payload := strings.Repeat("a", 10_000_000) + + var m1, m2 runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&m1) + + switch tc.mode { + case "compile": + _, iss := env.Compile(payload) + if iss == nil || iss.Err() == nil { + t.Fatal("expected size limit error, got nil") + } + if !strings.Contains(iss.Err().Error(), "expression code point size exceeds limit") { + t.Fatalf("unexpected error: %v", iss.Err()) + } + case "parse": + _, iss := env.Parse(payload) + if iss == nil || iss.Err() == nil { + t.Fatal("expected size limit error, got nil") + } + if !strings.Contains(iss.Err().Error(), "expression code point size exceeds limit") { + t.Fatalf("unexpected error: %v", iss.Err()) + } + } + + runtime.ReadMemStats(&m2) + allocDelta := (m2.TotalAlloc - m1.TotalAlloc) / (1024 * 1024) + if allocDelta > 5 { + t.Errorf("excessive memory allocation: %dMiB during %s (expected <5MiB with early enforcement)", + allocDelta, tc.mode) + } + t.Logf("[%s] memory delta: %dMiB", tc.mode, allocDelta) + }) + } +} diff --git a/cel/env.go b/cel/env.go index a7aa6db34..fbad943d6 100644 --- a/cel/env.go +++ b/cel/env.go @@ -21,6 +21,7 @@ import ( "slices" "strings" "sync" + "unicode/utf8" "github.com/google/cel-go/checker" chkdecls "github.com/google/cel-go/checker/decls" @@ -436,6 +437,35 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) { return ast, nil } +// defaultExpressionSizeCodePointLimit is the default maximum number of code points +// permitted in a CEL expression. This value must be kept in sync with the parser default. +const defaultExpressionSizeCodePointLimit = 100_000 + +// effectiveCodePointLimit returns the configured expression size code point limit, +// or the default limit if none has been configured. +func (e *Env) effectiveCodePointLimit() int { + if l := e.limits[limitCodePointSize]; l != 0 { + return l + } + return defaultExpressionSizeCodePointLimit +} + +// checkExpressionSize checks whether the input text exceeds the configured expression +// size code point limit before source construction to avoid allocating memory +// proportional to the full input size for oversized expressions. +func (e *Env) checkExpressionSize(txt string) *Issues { + limit := e.effectiveCodePointLimit() + size := utf8.RuneCountInString(txt) + if size > limit { + errs := common.NewErrors(common.NewTextSource("")) + errs.ReportErrorAtID(0, common.NoLocation, + "expression code point size exceeds limit: size: %d, limit %d", + size, limit) + return &Issues{errs: errs} + } + return nil +} + // Compile combines the Parse and Check phases CEL program compilation to produce an Ast and // associated issues. // @@ -445,6 +475,9 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) { // // Note, for parse-only uses of CEL use Parse. func (e *Env) Compile(txt string) (*Ast, *Issues) { + if iss := e.checkExpressionSize(txt); iss != nil { + return nil, iss + } return e.CompileSource(common.NewTextSource(txt)) } @@ -649,6 +682,9 @@ func (e *Env) Validators() []ASTValidator { // This form of Parse creates a Source value for the input `txt` and forwards to the // ParseSource method. func (e *Env) Parse(txt string) (*Ast, *Issues) { + if iss := e.checkExpressionSize(txt); iss != nil { + return nil, iss + } src := common.NewTextSource(txt) return e.ParseSource(src) }