Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 17 additions & 0 deletions cel/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,16 @@ func (e *Env) Check(ast *Ast) (*Ast, *Issues) {
return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo())
}

// Guard against ASTs that bypass the parser depth limit (e.g. loaded via
// ParsedExprToAst / CheckedExprToAst), since later recursive checking
// would otherwise risk a Go stack overflow on adversarially deep inputs.
if celast.ExceedsMaxDepth(ast.NativeRep().Expr(), celast.MaxNestingDepth) {
errs := common.NewErrors(ast.Source())
errs.ReportErrorString(common.NoLocation,
fmt.Sprintf("input exceeds maximum expression nesting depth: %d", celast.MaxNestingDepth))
return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo())
}

checked, errs := checker.Check(ast.NativeRep(), ast.Source(), chk)
if len(errs.GetErrors()) > 0 {
return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo())
Expand Down Expand Up @@ -693,6 +703,13 @@ func (e *Env) Program(ast *Ast, opts ...ProgramOption) (Program, error) {
// PlanProgram generates an evaluable instance of the AST in the go-native representation within
// the environment (Env).
func (e *Env) PlanProgram(a *celast.AST, opts ...ProgramOption) (Program, error) {
// Guard against ASTs that bypass the parser depth limit (e.g. loaded via
// ParsedExprToAst / CheckedExprToAst), since later recursive planning and
// evaluation would otherwise risk a Go stack overflow on adversarially
// deep inputs.
if a != nil && celast.ExceedsMaxDepth(a.Expr(), celast.MaxNestingDepth) {
return nil, fmt.Errorf("input exceeds maximum expression nesting depth: %d", celast.MaxNestingDepth)
}
optSet := e.progOpts
if len(opts) != 0 {
mergedOpts := []ProgramOption{}
Expand Down
58 changes: 58 additions & 0 deletions cel/io_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,61 @@ func TestCheckedExprToAstMissingInfo(t *testing.T) {
t.Fatalf("ast2.ResultType() got %v, wanted 'int'", ast.ResultType())
}
}

func TestLoadedAstDepthLimit(t *testing.T) {
env, err := NewEnv()
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}

// Sanity check: a shallow parsed expression still checks and plans clean.
shallow, iss := env.Parse("1 + 2")
if iss.Err() != nil {
t.Fatalf("Parse('1 + 2') failed: %v", iss.Err())
}
if _, iss := env.Check(shallow); iss.Err() != nil {
t.Fatalf("Check(shallow) failed: %v", iss.Err())
}
if _, err := env.Program(shallow); err != nil {
t.Fatalf("Program(shallow) failed: %v", err)
}

// Build a synthetic deeply nested AST (depth ~300) using iteratively
// stacked unary `!` calls, well above the 250 default but far below the
// Go stack limit so the test itself never crashes.
const depth = 300
expr := &exprpb.Expr{
Id: 1,
ExprKind: &exprpb.Expr_ConstExpr{
ConstExpr: &exprpb.Constant{
ConstantKind: &exprpb.Constant_BoolValue{BoolValue: true},
},
},
}
for i := 0; i < depth; i++ {
expr = &exprpb.Expr{
Id: int64(i + 2),
ExprKind: &exprpb.Expr_CallExpr{
CallExpr: &exprpb.Expr_Call{
Function: operators.LogicalNot,
Args: []*exprpb.Expr{expr},
},
},
}
}
deepAst := ParsedExprToAst(&exprpb.ParsedExpr{Expr: expr})
Comment thread
TristonianJones marked this conversation as resolved.
Outdated

_, iss = env.Check(deepAst)
if iss == nil || iss.Err() == nil {
t.Fatalf("Check(deepAst) expected an error, got nil")
}
if !strings.Contains(iss.Err().Error(), "maximum expression nesting depth") {
t.Errorf("Check(deepAst) error = %v, want it to mention 'maximum expression nesting depth'", iss.Err())
}

if _, err := env.Program(deepAst); err == nil {
t.Fatalf("Program(deepAst) expected an error, got nil")
} else if !strings.Contains(err.Error(), "maximum expression nesting depth") {
t.Errorf("Program(deepAst) error = %v, want it to mention 'maximum expression nesting depth'", err)
}
}
101 changes: 101 additions & 0 deletions common/ast/depth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ast

// MaxNestingDepth is the default maximum expression nesting depth applied to
// ASTs that enter through non-parser ingestion paths. It mirrors the parser's
// default maxRecursionDepth so loaded ASTs are validated against the same
// bound as ASTs produced from CEL source.
const MaxNestingDepth = 250
Comment thread
TristonianJones marked this conversation as resolved.
Outdated

// ExceedsMaxDepth reports whether the given expression nests deeper than
// maxDepth. The traversal itself is bounded: it never recurses past
// maxDepth+1 levels, so it is safe to call on adversarially deep inputs that
// would otherwise blow the Go stack during later checking or planning.
//
// A non-positive maxDepth disables the check and always returns false.
func ExceedsMaxDepth(e Expr, maxDepth int) bool {
Comment thread
TristonianJones marked this conversation as resolved.
Outdated
if maxDepth <= 0 {
return false
}
return exceedsMaxDepth(e, 0, maxDepth)
}

func exceedsMaxDepth(e Expr, depth, maxDepth int) bool {
if e == nil {
return false
}
if depth > maxDepth {
return true
}
switch e.Kind() {
case CallKind:
c := e.AsCall()
if c.IsMemberFunction() {
if exceedsMaxDepth(c.Target(), depth+1, maxDepth) {
return true
}
}
for _, arg := range c.Args() {
if exceedsMaxDepth(arg, depth+1, maxDepth) {
return true
}
}
case ComprehensionKind:
c := e.AsComprehension()
if exceedsMaxDepth(c.IterRange(), depth+1, maxDepth) {
return true
}
if exceedsMaxDepth(c.AccuInit(), depth+1, maxDepth) {
return true
}
if exceedsMaxDepth(c.LoopCondition(), depth+1, maxDepth) {
return true
}
if exceedsMaxDepth(c.LoopStep(), depth+1, maxDepth) {
return true
}
if exceedsMaxDepth(c.Result(), depth+1, maxDepth) {
return true
}
case ListKind:
for _, elem := range e.AsList().Elements() {
if exceedsMaxDepth(elem, depth+1, maxDepth) {
return true
}
}
case MapKind:
for _, entry := range e.AsMap().Entries() {
me := entry.AsMapEntry()
if exceedsMaxDepth(me.Key(), depth+1, maxDepth) {
return true
}
if exceedsMaxDepth(me.Value(), depth+1, maxDepth) {
return true
}
}
case SelectKind:
if exceedsMaxDepth(e.AsSelect().Operand(), depth+1, maxDepth) {
return true
}
case StructKind:
for _, f := range e.AsStruct().Fields() {
if exceedsMaxDepth(f.AsStructField().Value(), depth+1, maxDepth) {
return true
}
}
}
return false
}