surrealdb: COMPUTED fields expose values hidden by field-level SELECT permissions
What fixes it
- 3.3.0surrealdb, rustGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
A record user could read values hidden from them by field-level SELECT permissions through COMPUTED fields. On the default read path, a readable COMPUTED field derived from a hidden field carried that field's value. In the RETURN clause of a write, a COMPUTED field that was itself hidden could still be returned.
Reads. The streaming query planner (the default for read-only statements since 3.0.0) evaluated a record's COMPUTED fields before removing the fields the caller may not select, and did not evaluate them again afterwards. A readable COMPUTED field whose expression read a hidden stored field therefore kept that field's value:
DEFINE FIELD secret ON item PERMISSIONS FOR select NONE; DEFINE FIELD derived ON item COMPUTED secret PERMISSIONS FOR select FULL; -- As a record user: `secret` is removed, but `derived` still held its value. SELECT * FROM item:one;
The same happened when the expression read the field through the record's own id (COMPUTED id.secret), and when the record was reached through a record link or a graph traversal. The legacy executor evaluates COMPUTED fields again against the permission-reduced record, and returned NONE in these cases.
Writes. A COMPUTED field hidden by its own SELECT permission was removed from a statement's output only by a final pass that matched output field names. A write statement's RETURN clause could read the field through $before or $after, under an alias, or with RETURN VALUE, which put it where that pass did not look. For example, UPDATE person:one SET name = 'x' RETURN { before: $before, after: $after } returned the hidden field in both documents. Writes take this path whichever planner strategy is configured. The legacy executor's SELECT had the same gap for an aliased field or a WHERE condition on it.
Impact
Only schemas with COMPUTED fields are affected: either a readable COMPUTED field that reads a field hidden from the caller, or a COMPUTED field hidden by its own SELECT permission.
What an attacker can do:
- As a record user with SELECT on a table, read a hidden stored field wherever a readable
COMPUTEDfield on that table derives from it, just by selecting the record (default configuration). - As a record user allowed to create, update, upsert or delete records in a table, read a
COMPUTEDfield that is hidden from them, as it was before and after the write, through the statement'sRETURNclause. - Where a
SELECTran on the legacy executor, read or test such a hiddenCOMPUTEDfield through an alias or aWHEREcondition.
What it can't do:
- Read hidden fields that no
COMPUTEDfield reads: hidden stored fields are still removed from every row. - Reach records the caller could not already select or write. This issue concerns field-level permissions only, and does not bypass table-level permissions.
- Affect system users (root, namespace or database users), who bypass
PERMISSIONSclauses, or gain write access the caller did not already have (confidentiality only).
Patches
When a hidden stored field has been removed, streaming reads now evaluate COMPUTED fields again against the permission-reduced record before applying each COMPUTED field's own SELECT permission, which is the order the legacy executor uses. A COMPUTED field that reads its own record through id now sees that same reduced record, and never a value left in storage by a field that was later redefined as COMPUTED. Hidden COMPUTED fields are also removed from every permission-reduced document before a projection, a RETURN clause, $before/$after or a WHERE condition can read them.
- Versions 3.3.0 and later are not affected by this issue.
Workarounds
Users unable to patch should consider the following workarounds:
- For reads, force the legacy executor with
--planner-strategy compute-only(envSURREAL_PLANNER_STRATEGY, orPlannerStrategy::ComputeOnlyin the embedded SDK'sCapabilities). Before 3.3.0 the legacy executor let aCOMPUTEDfield that is itself hidden be read through an alias or aWHEREcondition, so use this only where noCOMPUTEDfield relies on its own SELECT permission. - Give a readable
COMPUTEDfield the same SELECT permission as the fields its expression reads. - Do not rely on a
COMPUTEDfield's own SELECT permission to hide it from users who can create, update, upsert or delete records in its table. The planner strategy does not affect writes. - Where record users do not need to send their own queries, deny them arbitrary queries with the
--deny-arbitrary-querycapability flag.
References
- SurrealQL Documentation — DEFINE FIELD
- SurrealQL Documentation — DEFINE TABLE … PERMISSIONS
Fix field-level select permissions on computed fields, array fields and RETURN DIFF(included in SurrealDB 3.3.0)fix(exec): re-derive computed fields against the reduced record(included in SurrealDB 3.3.0)fix(exec): resolve a computed field's self-dereference against the reduced row(included in SurrealDB 3.3.0)fix(exec): sanitise self-reads when a computed name shadows a stored key(included in SurrealDB 3.3.0)fix(exec): derive the self-read view where computed fields are evaluated(included in SurrealDB 3.3.0)
Acknowledgements
Thank you to santhreal for reporting this issue.
Text from GitHub Advisory Database
Affected versions 1
- surrealdb, rust>= 3.0.0, < 3.3.0GitHub