Skip to content
Merged
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
20 changes: 20 additions & 0 deletions cel/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,10 @@ func TestEnvToConfig(t *testing.T) {
opts: []EnvOption{
ExtendedValidations(),
ASTValidators(ValidateComprehensionNestingLimit(1)),
ASTValidators(ValidateBindNestingLimit(2)),
},
want: env.NewConfig("validators").AddValidators(
env.NewValidator("cel.validator.bind_nesting_limit").SetConfig(map[string]any{"limit": 2}),
env.NewValidator("cel.validator.comprehension_nesting_limit").SetConfig(map[string]any{"limit": 1}),
env.NewValidator("cel.validator.duration"),
env.NewValidator("cel.validator.homogeneous_literals"),
Expand Down Expand Up @@ -1023,6 +1025,24 @@ func TestEnvFromConfigErrors(t *testing.T) {
AddValidators(env.NewValidator("cel.validator.comprehension_nesting_limit").SetConfig(map[string]any{"limit": 2.5})),
want: errors.New("invalid validator: cel.validator.comprehension_nesting_limit, limit value is not a whole number: 2.5"),
},
{
name: "invalid cel_bind validator config",
conf: env.NewConfig("invalid validator config").
AddValidators(env.NewValidator("cel.validator.bind_nesting_limit")),
want: errors.New("invalid validator"),
},
{
name: "invalid cel_bind validator config type - unsupported type",
conf: env.NewConfig("invalid validator config").
AddValidators(env.NewValidator("cel.validator.bind_nesting_limit").SetConfig(map[string]any{"limit": "2"})),
want: errors.New("invalid validator"),
},
{
name: "invalid cel_bind validator config type - fractional",
conf: env.NewConfig("invalid validator config").
AddValidators(env.NewValidator("cel.validator.bind_nesting_limit").SetConfig(map[string]any{"limit": 2.5})),
want: errors.New("invalid validator: cel.validator.bind_nesting_limit, limit value is not a whole number: 2.5"),
},
}
for _, tst := range tests {
tc := tst
Expand Down
103 changes: 96 additions & 7 deletions cel/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ import (
)

const (
durationValidatorName = "cel.validator.duration"
regexValidatorName = "cel.validator.matches"
timestampValidatorName = "cel.validator.timestamp"
homogeneousValidatorName = "cel.validator.homogeneous_literals"
nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit"
durationValidatorName = "cel.validator.duration"
regexValidatorName = "cel.validator.matches"
timestampValidatorName = "cel.validator.timestamp"
homogeneousValidatorName = "cel.validator.homogeneous_literals"
nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit"
bindNestingLimitValidatorName = "cel.validator.bind_nesting_limit"

// HomogeneousAggregateLiteralExemptFunctions is the ValidatorConfig key used to configure
// the set of function names which are exempt from homogeneous type checks. The expected type
Expand Down Expand Up @@ -61,6 +62,23 @@ var (
}
return nil, fmt.Errorf("invalid validator: %s missing limit", nestingLimitValidatorName)
},
bindNestingLimitValidatorName: func(val *env.Validator) (ASTValidator, error) {
if limit, found := val.ConfigValue("limit"); found {
// In case of protos, config value is of type by google.protobuf.Value, which numeric values are always a double.
if val, isDouble := limit.(float64); isDouble {
if val != float64(int64(val)) {
return nil, fmt.Errorf("invalid validator: %s, limit value is not a whole number: %v", bindNestingLimitValidatorName, limit)
}
return ValidateBindNestingLimit(int(val)), nil
}

if val, isInt := limit.(int); isInt {
return ValidateBindNestingLimit(val), nil
}
return nil, fmt.Errorf("invalid validator: %s unsupported limit type: %v", bindNestingLimitValidatorName, limit)
}
return nil, fmt.Errorf("invalid validator: %s missing limit", bindNestingLimitValidatorName)
},
durationValidatorName: func(*env.Validator) (ASTValidator, error) {
return ValidateDurationLiterals(), nil
},
Expand Down Expand Up @@ -233,6 +251,13 @@ func ValidateComprehensionNestingLimit(limit int) ASTValidator {
return nestingLimitValidator{limit: limit}
}

// ValidateBindNestingLimit ensures that cel.bind() macro nesting does not exceed the specified limit.
//
// This validator can be useful for preventing arbitrarily nested cel.bind() macro calls.
func ValidateBindNestingLimit(limit int) ASTValidator {
return bindNestingLimitValidator{limit: limit}
}

type argChecker func(env *Env, call, arg ast.Expr) error

func newFormatValidator(funcName string, argNum int, check argChecker) formatValidator {
Expand Down Expand Up @@ -432,8 +457,7 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i
}
// When the comprehension has an empty range, continue to the next ancestor
// as this comprehension does not have any associated cost.
iterRange := e.AsComprehension().IterRange()
if iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 {
if isEmptyRangeComprehension(e) {
e, hasParent = e.Parent()
continue
}
Expand All @@ -447,3 +471,68 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i
}
}
}

type bindNestingLimitValidator struct {
limit int
}

// Name returns the name of the cel.bind nesting limit validator.
func (v bindNestingLimitValidator) Name() string {
return bindNestingLimitValidatorName
}

// ToConfig converts the ASTValidator to an env.Validator specifying the validator name and the nesting limit
// as an integer value: {"limit": int}
func (v bindNestingLimitValidator) ToConfig() *env.Validator {
return env.NewValidator(v.Name()).SetConfig(map[string]any{"limit": v.limit})
}

// Validate implements the ASTValidator interface method.
func (v bindNestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, iss *Issues) {
root := ast.NavigateAST(a)
comprehensions := ast.MatchDescendants(root, ast.KindMatcher(ast.ComprehensionKind))
var celBinds []ast.NavigableExpr
for _, comp := range comprehensions {
if isCelBind(comp) {
celBinds = append(celBinds, comp)
}
}
if len(celBinds) <= v.limit {
return
}
for _, comp := range celBinds {
count := 0
e := comp
hasParent := true
for hasParent {
if isCelBind(e) {
count++
if count > v.limit {
iss.ReportErrorAtID(comp.ID(), "cel.bind exceeds nesting limit")
break
}
}
e, hasParent = e.Parent()
}
}
}

func isEmptyRangeComprehension(e ast.NavigableExpr) bool {
if e.Kind() != ast.ComprehensionKind {
return false
}
iterRange := e.AsComprehension().IterRange()
return iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0
}

func isCelBind(e ast.NavigableExpr) bool {
if !isEmptyRangeComprehension(e) {
return false
}
compre := e.AsComprehension()
loopCond := compre.LoopCondition()
loopStep := compre.LoopStep()
return compre.IterVar() == unusedIterVar &&
loopCond.Kind() == ast.LiteralKind && loopCond.AsLiteral().Value() == false &&
loopStep.Kind() == ast.IdentKind && loopStep.AsIdent() == compre.AccuVar()
}
65 changes: 65 additions & 0 deletions ext/bindings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter"
"github.com/google/cel-go/test"
)

var bindingTests = []struct {
Expand Down Expand Up @@ -563,3 +564,67 @@ func BenchmarkBlockEval(b *testing.B) {
prg.Eval(input)
}
}

func TestValidateBindNestingLimit(t *testing.T) {
env, err := cel.NewEnv(
Bindings(),
cel.ASTValidators(cel.ValidateBindNestingLimit(2)),
)
if err != nil {
t.Fatalf("cel.NewEnv() failed: %v", err)
}
tests := []struct {
expr string
iss string
}{
{
expr: `cel.bind(a, 1, a + 1)`,
},
{
expr: `cel.bind(a, 1, cel.bind(b, 2, a + b))`,
},
{
// two cel.binds, but in separate branches
expr: `cel.bind(a, 1, a) + cel.bind(b, 2, b)`,
},
{
// empty iteration range comprehension (e.g. cel.bind) does not count against comprehension limit,
// but counts against cel.bind nesting limit.
expr: `[1, 2, 3].exists(i, cel.bind(a, i, cel.bind(b, a, a + b) > 0))`,
},
{
// three cel.binds, three levels deep
expr: `cel.bind(a, 1, cel.bind(b, 2, cel.bind(c, 3, a + b + c)))`,
iss: `
ERROR: <input>:1:39: cel.bind exceeds nesting limit
| cel.bind(a, 1, cel.bind(b, 2, cel.bind(c, 3, a + b + c)))
| ......................................^`,
},
{
// three cel.binds, three levels deep with non-comprehension ancestor (+)
expr: `cel.bind(a, 1, cel.bind(b, 2, 1 + cel.bind(c, 3, a + b + c)))`,
iss: `
ERROR: <input>:1:43: cel.bind exceeds nesting limit
| cel.bind(a, 1, cel.bind(b, 2, 1 + cel.bind(c, 3, a + b + c)))
| ..........................................^`,
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.expr, func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if tc.iss != "" {
if iss.Err() == nil {
t.Fatalf("env.Compile(%v) returned ast, expected error: %v", tc.expr, tc.iss)
}
if !test.Compare(iss.Err().Error(), tc.iss) {
t.Fatalf("env.Compile(%v) returned %v, expected error: %v", tc.expr, iss.Err(), tc.iss)
}
return
}
if iss.Err() != nil {
t.Fatalf("env.Compile(%v) failed: %v", tc.expr, iss.Err())
}
})
}
}