Found while implementing #10749 (read-time redaction of the approval payload snapshot). Filed rather than fixed there: #10749's scope is the serve path, and this is a predicate path whose fix changes search semantics.
Observation
ApprovalService.listRequests accepts a free-text q and pushes it down as a filter on the snapshot column:
{ payload_json: { $contains: q } }
payload_json holds the submitted record's full raw row. After #10749 the snapshot is redacted on the way out, keyed on the reading caller — but the filter is evaluated by the driver against the stored, unredacted column, before anything is served. So a caller who is correctly prevented from seeing a restricted field can still probe it: submit a candidate value, observe whether the request comes back, repeat. Row membership is the oracle; no restricted byte is ever rendered.
Why this is a real shape rather than a theoretical one
The platform already recognises exactly this class and closes it elsewhere. maskingRule (#8993) makes a field that a caller sees masked non-filterable / non-sortable / non-groupable / non-aggregatable, rejected loudly, and the spec states the reason in those terms — "otherwise equality probes reconstruct the hidden span". The approval search path is the same probe against the same declarations, reached through a different column.
Why it was not fixed on #10749
Closing it is not a redaction change:
- the filter cannot simply be dropped — free-text search over the snapshot is a shipped capability of the approvals inbox;
- narrowing it to the caller's readable fields requires the filter to be decomposed per field, which a
$contains over a serialized JSON blob does not express;
- refusing the query when the caller cannot read every snapshot field would change search behaviour for existing deployments.
Each of those is a design choice with a user-visible consequence, so it wants its own grading rather than riding a redaction PR.
Locator
packages/plugins/plugin-approvals/src/approval-service.ts — the free-text branch of listRequests' buildRequestWhere (search around the payload_json / $contains pair; the surrounding comment describes it as "Free-text search, pushed down").
Notes
- Not user-reported; found by inspection while inventorying the column's consumers.
- No assignee — recording, not claiming.
Found while implementing #10749 (read-time redaction of the approval payload snapshot). Filed rather than fixed there: #10749's scope is the serve path, and this is a predicate path whose fix changes search semantics.
Observation
ApprovalService.listRequestsaccepts a free-textqand pushes it down as a filter on the snapshot column:payload_jsonholds the submitted record's full raw row. After #10749 the snapshot is redacted on the way out, keyed on the reading caller — but the filter is evaluated by the driver against the stored, unredacted column, before anything is served. So a caller who is correctly prevented from seeing a restricted field can still probe it: submit a candidate value, observe whether the request comes back, repeat. Row membership is the oracle; no restricted byte is ever rendered.Why this is a real shape rather than a theoretical one
The platform already recognises exactly this class and closes it elsewhere.
maskingRule(#8993) makes a field that a caller sees masked non-filterable / non-sortable / non-groupable / non-aggregatable, rejected loudly, and the spec states the reason in those terms — "otherwise equality probes reconstruct the hidden span". The approval search path is the same probe against the same declarations, reached through a different column.Why it was not fixed on #10749
Closing it is not a redaction change:
$containsover a serialized JSON blob does not express;Each of those is a design choice with a user-visible consequence, so it wants its own grading rather than riding a redaction PR.
Locator
packages/plugins/plugin-approvals/src/approval-service.ts— the free-text branch oflistRequests'buildRequestWhere(search around thepayload_json/$containspair; the surrounding comment describes it as "Free-text search, pushed down").Notes