From 350894b4049b69e4ea981939b69f733759534318 Mon Sep 17 00:00:00 2001 From: Ville Vesilehto Date: Sun, 22 Feb 2026 20:36:30 +0200 Subject: [PATCH] fix: reject unexported struct fields at runtime When the type checker cannot determine a concrete struct type at compile time (e.g. ternary with mixed types), field access falls through to dynamic OpFetch at runtime. The Fetch function in vm/runtime and the get function in builtin/lib used FieldByNameFunc which matched unexported fields, then called Interface() on the resulting reflect.Value, causing a reflect error. Add IsExported() guards after FieldByNameFunc in both Fetch() and get() so unexported fields are never accessed via reflection. Signed-off-by: Ville Vesilehto --- builtin/lib.go | 14 +++++---- test/issues/934/issue_test.go | 53 +++++++++++++++++++++++++++++++++++ vm/runtime/runtime.go | 2 +- 3 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 test/issues/934/issue_test.go diff --git a/builtin/lib.go b/builtin/lib.go index d626f944..61748da0 100644 --- a/builtin/lib.go +++ b/builtin/lib.go @@ -596,9 +596,10 @@ func get(params ...any) (out any, err error) { case reflect.Struct: fieldName := i.(string) - value := v.FieldByNameFunc(func(name string) bool { - field, _ := v.Type().FieldByName(name) - switch field.Tag.Get("expr") { + t := v.Type() + field, ok := t.FieldByNameFunc(func(name string) bool { + f, _ := t.FieldByName(name) + switch f.Tag.Get("expr") { case "-": return false case fieldName: @@ -607,8 +608,11 @@ func get(params ...any) (out any, err error) { return name == fieldName } }) - if value.IsValid() { - return value.Interface(), nil + if ok && field.IsExported() { + value := v.FieldByIndex(field.Index) + if value.IsValid() { + return value.Interface(), nil + } } } diff --git a/test/issues/934/issue_test.go b/test/issues/934/issue_test.go new file mode 100644 index 00000000..15ac2627 --- /dev/null +++ b/test/issues/934/issue_test.go @@ -0,0 +1,53 @@ +package issue934 + +import ( + "testing" + + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/internal/testify/require" +) + +type Env struct { + Exported + unexported +} + +type Exported struct { + Str string + str string +} + +type unexported struct { + Integer int + integer int +} + +// TestIssue934 tests that accessing unexported fields on values whose type +// is unknown at compile time (e.g., from a ternary with mixed types) yields +// a descriptive error. +// +// OSS-Fuzz issue #486370271. +func TestIssue934(t *testing.T) { + env := map[string]any{ + "v": Env{}, + } + + // Accessing unexported fields like time.Time.loc + // should produce a proper error. + _, err := expr.Eval(`(true ? v : "string").str`, env) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot fetch str") + + // Exported field access should still work. + _, err = expr.Eval(`(true ? v : "string").Str`, env) + require.NoError(t, err) + + // Access unexported field inherited from unexported embedded struct. + _, err = expr.Eval(`(true ? v : "string").integer`, env) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot fetch integer") + + // Access exported field inherited from unexported embedded struct should work. + _, err = expr.Eval(`(true ? v : "string").Integer`, env) + require.NoError(t, err) +} diff --git a/vm/runtime/runtime.go b/vm/runtime/runtime.go index d24c6af0..bc6f2b4d 100644 --- a/vm/runtime/runtime.go +++ b/vm/runtime/runtime.go @@ -90,7 +90,7 @@ func Fetch(from, i any) any { return name == fieldName } }) - if ok { + if ok && field.IsExported() { value := v.FieldByIndex(field.Index) if value.IsValid() { fieldCache.Store(key, field.Index)