Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"os"
"reflect"
"runtime"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -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)
})
}
}
36 changes: 36 additions & 0 deletions cel/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"slices"
"strings"
"sync"
"unicode/utf8"

"github.com/google/cel-go/checker"
chkdecls "github.com/google/cel-go/checker/decls"
Expand Down Expand Up @@ -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.
//
Expand All @@ -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))
}

Expand Down Expand Up @@ -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)
}
Expand Down