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