CVE-2022-36085 (Go, published 2022-09-08T14:15Z)
Description: value: Open Policy Agent (OPA) is an open source, general-purpose policy engine. The Rego compiler provides a (deprecated) `WithUnsafeBuiltins` function, which allows users to provide a set of built-in functions that should be deemed unsafe — and as such rejected — by the compiler if encountered in the policy compilation stage. A bypass of this protection has been found, where the use of the `with` keyword to mock such a built-in function (a feature introduced in OPA v0.40.0), isn’t taken into account by `WithUnsafeBuiltins`. Multiple conditions need to be met in order to create an adverse effect. Version 0.43.1 contains a patch for this issue. As a workaround, avoid using the `WithUnsafeBuiltins` function and use the `capabilities` feature instead.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/open-policy-agent/opa
Files changed:
ast/compile.go
ast/compile_test.go
rego/rego_test.go
Commit message: ast/compile: respect unsafeBuiltinMap for 'with' replacements (#5107)

The changes are necessary for both the Compiler and the QueryCompiler. Tests
have been added to ensure that the code path through the rego package has also
been fixed.

Fixes CVE-2022-36085.

Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>

Vulnerable code:
			body, err := rewriteWithModifiersInBody(c, f, body)
	var unsafe map[string]struct{}
	if qc.unsafeBuiltins != nil {
		unsafe = qc.unsafeBuiltins
	} else {
		unsafe = qc.compiler.unsafeBuiltinsMap
	}
	errs := checkUnsafeBuiltins(unsafe, body)
	body, err := rewriteWithModifiersInBody(qc.compiler, f, body)
func rewriteWithModifiersInBody(c *Compiler, f *equalityFactory, body Body) (Body, *Error) {
		exprs, err := rewriteWithModifier(c, f, body[i])
func rewriteWithModifier(c *Compiler, f *equalityFactory, expr *Expr) ([]*Expr, *Error) {
		eval, err := validateWith(c, expr, i)
func validateWith(c *Compiler, expr *Expr, i int) (bool, *Error) {
						if validateWithFunctionValue(c.builtins, c.RuleTree, value) {
							return false, nil
	case isBuiltinRefOrVar(c.builtins, target):
		if validateWithFunctionValue(c.builtins, c.RuleTree, value) {
			return false, nil
func validateWithFunctionValue(bs map[string]*Builtin, ruleTree *TreeNode, value *Term) bool {
			return true
	return isBuiltinRefOrVar(bs, value)
func isBuiltinRefOrVar(bs map[string]*Builtin, term *Term) bool {
		return ok
	return false
	c := NewCompiler().WithUnsafeBuiltins(map[string]struct{}{
		"count": {},
	})
	_, err := c.QueryCompiler().WithUnsafeBuiltins(map[string]struct{}{}).Compile(MustParseBody("count([])"))
	if err != nil {
		t.Fatal(err)

Fixed code:
			body, err := rewriteWithModifiersInBody(c, c.unsafeBuiltinsMap, f, body)
	errs := checkUnsafeBuiltins(qc.unsafeBuiltinsMap(), body)
func (qc *queryCompiler) unsafeBuiltinsMap() map[string]struct{} {
	if qc.unsafeBuiltins != nil {
		return qc.unsafeBuiltins
	}
	return qc.compiler.unsafeBuiltinsMap
}

	body, err := rewriteWithModifiersInBody(qc.compiler, qc.unsafeBuiltinsMap(), f, body)
func rewriteWithModifiersInBody(c *Compiler, unsafeBuiltinsMap map[string]struct{}, f *equalityFactory, body Body) (Body, *Error) {
		exprs, err := rewriteWithModifier(c, unsafeBuiltinsMap, f, body[i])
func rewriteWithModifier(c *Compiler, unsafeBuiltinsMap map[string]struct{}, f *equalityFactory, expr *Expr) ([]*Expr, *Error) {
		eval, err := validateWith(c, unsafeBuiltinsMap, expr, i)
func validateWith(c *Compiler, unsafeBuiltinsMap map[string]struct{}, expr *Expr, i int) (bool, *Error) {
	isBuiltinRefOrVar, err := isBuiltinRefOrVar(c.builtins, unsafeBuiltinsMap, target)
	if err != nil {
		return false, err
	}
						if ok, err := validateWithFunctionValue(c.builtins, unsafeBuiltinsMap, c.RuleTree, value); err != nil || ok {
							return false, err // may be nil
	case isBuiltinRefOrVar:
		if ok, err := validateWithFunctionValue(c.builtins, unsafeBuiltinsMap, c.RuleTree, value); err != nil || ok {
			return false, err // may be nil
func validateWithFunctionValue(bs map[string]*Builtin, unsafeMap map[string]struct{}, ruleTree *TreeNode, value *Term) (bool, *Error) {
			return true, nil
	return isBuiltinRefOrVar(bs, unsafeMap, value)
func isBuiltinRefOrVar(bs map[string]*Builtin, unsafeBuiltinsMap map[string]struct{}, term *Term) (bool, *Error) {
		if _, ok := unsafeBuiltinsMap[v.String()]; ok {
			return false, NewError(CompileErr, term.Location, "with keyword replacing built-in function: target must not be unsafe: %q", v)
		}
		return ok, nil
	return false, nil
		opts         func(*Compiler) *Compiler
		{
			note: "built-in function: replaced by another built-in that's marked unsafe",
			input: `
				q := is_object({"url": "https://httpbin.org", "method": "GET"})
				p { q with is_object as http.send }
			`,
			opts:    func(c *Compiler) *Compiler { return c.WithUnsafeBuiltins(map[string]struct{}{"http.send": {}}) },
			wantErr: fmt.Errorf("rego_compile_error: with keyword replacing built-in function: target must not be unsafe: \"http.send\""),
		},
		{
			note: "non-built-in function: replaced by another built-in that's marked unsafe",
			input: `
			r(_) = {}
			q := r({"url": "https://httpbin.org", "method": "GET"})
			p {
				q with r as http.send
			}`,
			opts:    func(c *Compiler) *Compiler { return c.WithUnsafeBuiltins(map[string]struct{}{"http.send": {}}) },
			wantErr: fmt.Errorf("rego_compile_error: with keyword replacing built-in function: target must not be unsafe: \"http.send\""),
		},
			if tc.opts != nil {
				c = tc.opts(c)
			}
	tests := []struct {
		note     string
		query    string
		compiler *Compiler
		opts     func(QueryCompiler) QueryCompiler
		err      string
	}{
		{
			note:     "builtin unsafe via compiler",
			query:    "count([])",
			compiler: NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}}),
			err:      "unsafe built-in function calls in expression: count",
		},
		{
			note:     "builtin unsafe via query compiler",
			query:    "count([])",
			compiler: NewCompiler(),
			opts: func(qc QueryCompiler) QueryCompiler {
				return qc.WithUnsafeBuiltins(map[string]struct{}{"count": {}})
			},
			err: "unsafe built-in function calls in expression: count",
		},
		{
			note:     "builtin unsafe via compiler, 'with' mocking",
			query:    "is_array([]) with is_array as count",
			compiler: NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}}),
			err:      `with keyword replacing built-in function: target must not be unsafe: "count"`,
		},
		{
			note:     "builtin unsafe via query compiler,  'with' mocking",
			query:    "is_array([]) with is_array as count",
			compiler: NewCompiler(),
			opts: func(qc QueryCompiler) QueryCompiler {
				return qc.WithUnsafeBuiltins(map[string]struct{}{"count": {}})
			},
			err: `with keyword replacing built-in function: target must not be unsafe: "count"`,
		},
	}
	for _, tc := range tests {
		t.Run(tc.note, func(t *testing.T) {
			qc := tc.compiler.QueryCompiler()
			if tc.opts != nil {
				qc = tc.opts(qc)
			}
			_, err := qc.Compile(MustParseBody(tc.query))
			var errs Errors
			if !errors.As(err, &errs) {
				t.Fatalf("expected error type %T, got %v %[2]T", errs, err)
			}
			if exp, act := 1, len(errs); exp != act {
				t.Fatalf("expected %d error(s), got %d", exp, act)
			}
			if exp, act := tc.err, errs[0].Message; exp != act {
				t.Errorf("expected message %q, got %q", exp, act)
			}
		})
	unsafeCountExprWith := `with keyword replacing built-in function: target must not be unsafe: "count"`
	t.Run("unsafe query, 'with' replacement", func(t *testing.T) {
		r := New(
			Query(`is_array([1, 2, 3]) with is_array as count`),
			UnsafeBuiltins(map[string]struct{}{"count": {}}),
		)
		if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExprWith) {
			t.Fatalf("Expected unsafe built-in error but got %v", err)
		}
	})

	t.Run("unsafe module, 'with' replacement in query", func(t *testing.T) {
		r := New(
			Query(`data.pkg.deny with is_array as count`),
			Module("pkg.rego", `package pkg
			deny {
				is_array(input.requests) > 10
			}
			`),
			UnsafeBuiltins(map[string]struct{}{"count": {}}),
		)
		if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExprWith) {
			t.Fatalf("Expected unsafe built-in error but got %v", err)
		}
	})

	t.Run("unsafe module, 'with' replacement in module", func(t *testing.T) {
		r := New(
			Query(`data.pkg.deny`),
			Module("pkg.rego", `package pkg
			deny {
				is_array(input.requests) > 10 with is_array as count
			}
			`),
			UnsafeBuiltins(map[string]struct{}{"count": {}}),
		)
		if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExprWith) {
			t.Fatalf("Expected unsafe built-in error but got %v", err)
		}
	})

	t.Run("inherit in query, 'with' replacement", func(t *testing.T) {
		r := New(
			Compiler(ast.NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}})),
			Query("is_array([]) with is_array as count"),
		)
		if _, err := r.Eval(ctx); err == nil || !strings.Contains(err.Error(), unsafeCountExprWith) {
			t.Fatalf("Expected unsafe built-in error but got %v", err)
		}
	})