Skip to content

security(policy): row-filter claim templates fail open on _eq/_neq/_gt/_lt when the claim is missing — only _in fails closed #385

Description

@taitelee

Summary

An unresolvable {{ jwt.* }} template in a policy row-filter renders as the empty string and emits a real predicate against '', instead of denying. Only _in was given the fail-closed treatment (#224); _eq/_neq/_gt/_lt fail open.

Detail

internal/policy/policy.go:

  • resolveTemplate (:266-279) returns "" when a claim path can't be resolved.
  • resolveFilters (:226-244) binds that "" directly for _eq/_neq/_gt/_lt.
  • _in alone emits 1 = 0 for an empty/unresolvable set (:248-253).

Example policy: select: { user: { filter: { tenant_id: { _eq: "{{ jwt.tenant_id }}" } } } }. A validly-signed token with role: user but no tenant_id claim (mixed IdP audiences, service tokens) yields WHERE tenant_id = '' → leaks every row whose tenant_id is empty. Worse, _neq/_gt on a string column (col != '' / col > '') matches essentially all rows — the restriction evaporates entirely.

The docs promise the opposite: docs/src/content/docs/access-control.mdx:233 says an unresolvable claim "matches no tenant and sees nothing" — only true for _in today.

Fix direction

When a filter template resolves to empty (claim absent), fail closed (1 = 0) rather than binding '', matching the _in behavior and the documented contract.

Found in a repo-wide audit; verified by code trace. Sibling of closed #224 (which fixed only _in) and #371 (check-path operators, not the filter path).

Activity

  1. added
    bugSomething isn't working
    area/policyAccess control policies (Hasura-style)
    securitySecurity-sensitive issue or fix
    on Jul 8, 2026
  2. coderabbitai commented on Jul 8, 2026

    @coderabbitai
    🔗 Related PRs

    #172 - feat(rbac)!: fail-closed authorization + default_role public access [closed]
    #330 - fix(pipes): escape non-scalar param values [closed]
    #357 - fix(policy): case-normalize aggregation before denied_aggregations check [merged]
    #358 - fix(policy): enforce _in on filter and check paths [merged]
    #378 - feat(auth): non-JWT operator key for admin + break-glass access [merged]


    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  3. EricAndrechek commented on Aug 3, 2026

    @EricAndrechek
    Member

    Escalated to P0 on the launch board (was P1). This is the fail-open row-level-security class the launch rubric gates the tag on (cf. #223): an unresolvable {{ jwt.* }} claim renders to "" and still emits a real predicate, so _neq/_gt/_lt on a string column (col != '') match ~all rows and the RLS restriction evaporates — a data-exposure default, and access-control.mdx documents the opposite. Only _in was fixed (#224 → #358); _eq/_neq/_gt/_lt still fail open. Flagging as a v0.1.0-alpha.1 flip-blocker per the #149 launch triage.

  4. moved this from Backlog to Ready in WaveHouse Task Boardon Aug 3, 2026
  5. moved this from Ready to In progress in WaveHouse Task Boardon Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/policyAccess control policies (Hasura-style)breaking-changeBreaking change to public API, CLI, or configbugSomething isn't workingsecuritySecurity-sensitive issue or fix

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions