You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(objectql): reject aggregations over secret/password fields (#3171)
`find`/`findOne` mask credential fields (secret always, password on
non-better-auth objects) so plaintext never leaves the engine, but
`aggregate()` had no equivalent guard — a GROUP BY / MIN / MAX / array_agg
over a `secret` or `password` column would surface the stored `secret:<id>`
ref or the password value. Post-hoc masking of aggregate output would
corrupt group keys, so the correct fix is to refuse the aggregation.
Add a fail-closed gate at the top of `ObjectQL.aggregate()` that scans the
two output-bearing positions on the aggregate query — `aggregations[].field`
(skipping COUNT(*)) and `groupBy` (string or `{ field }` bucket) — and throws
when any references a credential field. The gate keys off a new unconditional
`collectCredentialFields` collector (secret OR password, ignoring
`managedBy`): unlike read-masking, aggregating a credential is never allowed,
even on a better-auth object, where it would be an inference oracle over
hashes. Keeping the two collectors separate stops the concerns from drifting.
No legitimate caller aggregates a credential field (verified: the only
schema-field rollup path, recomputeSummaries, targets numeric fields like
`amount`/`estimate_hours`). Follows the http-dispatcher FLS aggregate gate.
Tests cover: secret/password rejected as measure, as string groupBy, and as
structured `{ field }` groupBy; no false positive when the field is
unreferenced; and rejection on a better-auth object. Closes#3171; the gap
was recorded as a follow-up in ADR-0100.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJrKxe1jXGudFT3nUks1yN
0 commit comments