From 9294dcff6f1ea33277ee4f66d46260017f7fb861 Mon Sep 17 00:00:00 2001 From: Koda Reef Date: Sat, 4 Apr 2026 19:44:04 +0000 Subject: [PATCH] fix: has() on non-container types returns error instead of false has() applied to a non-container, non-optional value (e.g., an integer or string) previously returned false silently when errorOnBadPresenceTest was not enabled. This caused !has(x.field) to evaluate to true when x was an unexpected type, creating a fail-open condition in policy expressions that use presence guards on dynamic inputs. This change narrows the default case in refQualify so that: - has() on a non-container, non-optional type (presenceOnly=true): returns missingKey error, preventing silent false returns - has() on an optional value (e.g. optional.none()): continues to return false, preserving correct optional semantics - Optional field selection (x.?field, presenceOnly=false): continues to return not-present for optional.none() compatibility - EnableErrorOnBadPresenceTest(true): errors for all cases (unchanged) No test expectations changed. Full suite green. Signed-off-by: Koda Reef --- cel/cel_test.go | 17 +++++++++++++++++ interpreter/attributes.go | 8 +++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cel/cel_test.go b/cel/cel_test.go index 31b2a318..ab4f21a6 100644 --- a/cel/cel_test.go +++ b/cel/cel_test.go @@ -2869,6 +2869,23 @@ func TestOptionalValuesEval(t *testing.T) { expr: `has({?'foo': optional.none()}.foo.value)`, out: "no such key: foo", }, + // has() on non-container types errors instead of returning false. + { + expr: `has(dyn(42).field)`, + out: "no such key: field", + }, + { + expr: `has(dyn('hello').field)`, + out: "no such key: field", + }, + { + expr: `has(dyn(true).field)`, + out: "no such key: field", + }, + { + expr: `has(dyn(null).field)`, + out: "no such key: field", + }, { expr: `{}.?invalid`, out: types.OptionalNone, diff --git a/interpreter/attributes.go b/interpreter/attributes.go index 6b8b5c1b..3b84fa3b 100644 --- a/interpreter/attributes.go +++ b/interpreter/attributes.go @@ -1419,7 +1419,13 @@ func refQualify(adapter types.Adapter, obj any, idx ref.Val, presenceTest, prese return val, true, nil default: if presenceTest && !errorOnBadPresenceTest { - return nil, false, nil + // Optional values and optional field selection (presenceOnly=false) + // report not-present for non-container types. has() macro + // (presenceOnly=true) on non-optional primitives falls through + // to missingKey to avoid silent false on bad presence tests. + if _, isOpt := celVal.(*types.Optional); isOpt || !presenceOnly { + return nil, false, nil + } } return nil, false, missingKey(idx) }