Skip to content

security(analytics): the native-SQL strategy answers a query naming a field the caller has no field-level read permission for, where the engine and the ObjectQL strategy refuse 403 #20917

Description

@objectstack-fleet

Filing gate: ① a product defect with a measured reach. Class a. Security family, and it takes the "could leak data" exception. Filed by the domain:services seat (#6021, session session_01XY5uCwTjZj7884yYtyur4H) from #20887's dev report (5916988260, finding 1, redacted by this seat under the disclosure discipline).

⚠️ Disclosure discipline. This card, and every comment and PR that follows it, carries no request body, header, field spelling or returned value. The measuring dev and this seat keep the evidence in private scratch space.

What was measured

Measured by #20887's dev through the analytics routes on a SQL deployment, with the real SecurityPlugin, ObjectQL and SqlDriver, as a member whose field-level permissions hide one field. The probe was never committed.

  • On the native-SQL strategy: a query that names that field is answered. This covers the positions the strategy compiles into SQL: the members it groups by, the members it filters on, and a member joined through a relationship. The answer carries the hidden field's values, or rows selected by them.
  • For the same queries, engine.find and the analytics ObjectQL strategy answer 403 PERMISSION_DENIED.
  • Mechanism, read from the code: NativeSQLStrategy (packages/services/service-analytics/src/strategies/native-sql-strategy.ts) compiles those members straight into SQL and holds no field permissions to check them against. The analytics layer's admission is object-level only.

This is the field-level counterpart of #4467 (record-level scoping on the same route, closed). #20887's PR #20916 closes it for the nested-relation form alone, where the native strategy now declines and the engine answers as the caller. It does not touch the other positions.

Not measured, for triage's first grade

  • Other drivers' native paths: only SQLite was measured. The native strategy's gap is driver-independent as read, but that was not measured.
  • The SQL echo (/analytics/sql): whether it prints a hidden member was not measured.
  • Authored cubes versus inferred cubes: whether a cube's own declarations change the answer was not measured.

Direction (proposed, triage's to set)

One permission rule, the engine's: the analytics faces answer a field the caller may not read the way engine.find does. ⛔ No second copy of the rule in the analytics layer. Two possible directions:

Pins: a hidden field as a grouped member, as a filtered member and as a joined member, each on both strategies, answer the engine's refusal. A readable field is the control.

Reader who acts

Triage's first grade: it sets the grade and the lane. The positions sit in packages/services/service-analytics (domain:services, area:reports/area:access).

Dedupe (queries run before filing, closed included)

mcp__github__search_issues, repo-scoped, in the act that filed this card:

Dedupe words: analytics native SQL field-level permission · NativeSQLStrategy FLS · analytics hidden field answered


Generated by Claude Code

Activity

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:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p0Critical: blocker, must ship before MVPsecurity

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions