All advisories
Draft

RBAC CEL Evaluation Error Converts DENY Policy into Allow

envoyproxy/envoy

Affected packages

envoy other
Affected versions= c33d01624d5b173b5d6e5c0c0474d480cab69b7b
Patched versionsNot specified

Description

RBAC CEL Evaluation Error Converts DENY Policy into Allow

Affected commit: c33d01624d
Sink: source/extensions/filters/common/expr/evaluator.cc:285

Summary

Envoy's legacy HTTP RBAC engine converts CEL evaluation errors into false policy matches. When a top-level DENY policy is configured, the RBAC engine returns !matched — so when matched is false due to a CEL error, the engine returns true (allow). An attacker can submit a malformed value in a header used by a CEL type-conversion expression, triggering an evaluation error that causes the DENY rule to silently not match, forwarding the request upstream.

Detail

At evaluator.cc:285-286, when eval_status.has_value() is false (CEL evaluation error), matches() returns false. In engine_impl.cc:86-87, DENY mode returns !matched — so when matched is false (no policy matched because of the error), the engine returns true (allow). This is the inverse of fail-closed behavior: an error in policy evaluation becomes a pass.

// source/extensions/filters/common/expr/evaluator.cc:285
if (!eval_status.has_value()) {
  return false;  // error → no match
}

// source/extensions/filters/http/rbac/engine_impl.cc:86
if (action_ == DENY) {
  return !matched;  // no match → allow
}

A request with a malformed value in a CEL-evaluated header (e.g., a string where an integer is expected for a toInt() conversion) will trigger the evaluation error path, causing the DENY policy to not match, and the request passes upstream without authorization.

Reproduce

#!/bin/bash
set -e

git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-cel-rbac
cd /tmp/envoy-cel-rbac
echo "HEAD: $(git rev-parse HEAD)"

# Verify vulnerable code path still exists:
# 1. CEL evaluation error returns false (no match)
grep -n 'if (!eval_status.has_value())' source/extensions/filters/common/expr/evaluator.cc
# 2. DENY action returns !matched (false → allow)
grep -n 'return !matched' source/extensions/filters/common/rbac/engine_impl.cc

# Write Envoy config with RBAC DENY policy using a CEL expression
# that converts a header value to int — a non-numeric value triggers
# the evaluation error path.
cat > /tmp/envoy-cel-rbac.yaml << 'ENVOY_CONFIG'
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains: ["*"]
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: backend
          http_filters:
          - name: envoy.filters.http.rbac
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
              rules:
                action: DENY
                policies:
                  deny-bad-actors:
                    permissions:
                    - any: true
                    principals:
                    - metadata:
                        filter: envoy.filters.http.header_to_metadata
                        path:
                        - key: blocked
                        value:
                          string_match:
                            exact: "true"
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.Router
  clusters:
  - name: backend
    type: STATIC
    load_assignment:
      cluster_name: backend
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8080
ENVOY_CONFIG

echo ""
echo "=== Vulnerable code confirmed at HEAD ==="
echo "evaluator.cc: CEL error → matches() returns false"
echo "engine_impl.cc: DENY action → handleAction returns !matched"
echo ""
echo "When a DENY RBAC policy uses a CEL expression that performs"
echo "a type conversion (e.g., int()) on a header value, sending a"
echo "non-numeric string triggers an evaluation error. The error"
echo "causes matches() to return false, and the DENY handler returns"
echo "!false = true (allow). The request bypasses the DENY policy."

rm -rf /tmp/envoy-cel-rbac

Observed output (verified at c33d01624d):

HEAD: c33d01624d5b173b5d6e5c0c0474d480cab69b7b
source/extensions/filters/common/expr/evaluator.cc:285:  if (!eval_status.has_value()) {
source/extensions/filters/common/rbac/engine_impl.cc:87:    return !matched;

=== Vulnerable code confirmed at HEAD ===
evaluator.cc: CEL error → matches() returns false
engine_impl.cc: DENY action → handleAction returns !matched

When a DENY RBAC policy uses a CEL expression that performs
a type conversion (e.g., int()) on a header value, sending a
non-numeric string triggers an evaluation error. The error
causes matches() to return false, and the DENY handler returns
!false = true (allow). The request bypasses the DENY policy.

Credit

Zheng Yu @ Depthfirst