GHSA-C8JX-96C9-8XRP
Vulnerability from github – Published: 2026-07-01 20:22 – Updated: 2026-07-01 20:22A record user could learn the value of a hidden field by counting how many records match a guess.
When DEFINE FIELD ... PERMISSIONS FOR select WHERE ... hides a field's contents from a caller, and that field is indexed, running SELECT count() FROM t WHERE hidden_field = "guess" GROUP ALL returned a count greater than zero whenever a record actually had that value — even though the caller was never allowed to read the field directly. The query planner used an indexed-COUNT shortcut (Index::Count, IndexCountScan, or the legacy Iterate Index Count / Iterate Index Keys paths) that counts matching index entries and skips the permission check that would normally hide the value. The same query with WITH NOINDEX correctly returned [], confirming the gap.
By repeating the count query with different guesses, an attacker can confirm or recover the contents of any restricted field they could not read through a normal SELECT.
Impact
What an attacker can do:
- Confirm or recover values of a field protected by field-level SELECT permissions on any table they hold table-level SELECT on, provided the field is indexed.
- Repeat the query with different guesses to read restricted field contents one value at a time.
What it can't do:
- Read fields that are not indexed (the shortcut only fires when an index covers the predicate column).
- Cross table, database or namespace isolation boundaries.
- Modify data, escalate privileges, or affect availability.
Patches
The legacy planner (surrealdb/core/src/idx/planner/tree.rs) and the streaming planner (surrealdb/core/src/exec/planner/select/mod.rs) now both refuse the indexed fast path when the WHERE / ORDER tree references a field governed by a non-Full SELECT permission:
resolve_indexesskips any B-tree / unique index whose columns are governed by such a permission.- A new
cond_touches_restricted_fieldflag is propagated;eval_countrefuses a dedicatedIndex::Countwhen set. - The streaming planner adds
cond_touches_restricted_select_field, aRestrictedIdiomCheckervisitor that matches each idiom against the table's field-permission prefixes (loaded via the plan-time txn), and gatesIndexCountScanemission on it. - The fast paths are preserved for root / owner sessions via
should_check_perms_for_view.
Versions 3.1.0 and later are not affected.
Workarounds
Users unable to patch are advised to consider the following workarounds:
- Avoid
DEFINE INDEXon fields whose values are protected by field-level SELECT permissions. The class of attack is specific to the indexed fast paths. - Restrict the ability of record users to issue arbitrary
SELECT count() … GROUP ALLqueries against tables containing field-protected columns. - Use namespace / database isolation as the primary boundary where feasible.
{
"affected": [
{
"package": {
"ecosystem": "crates.io",
"name": "surrealdb"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-01T20:22:06Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "A record user could learn the value of a hidden field by counting how many records match a guess.\n\nWhen `DEFINE FIELD ... PERMISSIONS FOR select WHERE ...` hides a field\u0027s contents from a caller, and that field is indexed, running `SELECT count() FROM t WHERE hidden_field = \"guess\" GROUP ALL` returned a count greater than zero whenever a record actually had that value \u2014 even though the caller was never allowed to read the field directly. The query planner used an indexed-COUNT shortcut (`Index::Count`, `IndexCountScan`, or the legacy `Iterate Index Count` / `Iterate Index Keys` paths) that counts matching index entries and skips the permission check that would normally hide the value. The same query with `WITH NOINDEX` correctly returned `[]`, confirming the gap.\n\nBy repeating the count query with different guesses, an attacker can confirm or recover the contents of any restricted field they could not read through a normal `SELECT`.\n\n### Impact\n\nWhat an attacker **can** do:\n\n- Confirm or recover values of a field protected by field-level SELECT permissions on any table they hold table-level SELECT on, provided the field is indexed.\n- Repeat the query with different guesses to read restricted field contents one value at a time.\n\nWhat it **can\u0027t** do:\n\n- Read fields that are not indexed (the shortcut only fires when an index covers the predicate column).\n- Cross table, database or namespace isolation boundaries.\n- Modify data, escalate privileges, or affect availability.\n\n### Patches\n\nThe legacy planner (`surrealdb/core/src/idx/planner/tree.rs`) and the streaming planner (`surrealdb/core/src/exec/planner/select/mod.rs`) now both refuse the indexed fast path when the WHERE / ORDER tree references a field governed by a non-`Full` SELECT permission:\n\n- `resolve_indexes` skips any B-tree / unique index whose columns are governed by such a permission.\n- A new `cond_touches_restricted_field` flag is propagated; `eval_count` refuses a dedicated `Index::Count` when set.\n- The streaming planner adds `cond_touches_restricted_select_field`, a `RestrictedIdiomChecker` visitor that matches each idiom against the table\u0027s field-permission prefixes (loaded via the plan-time txn), and gates `IndexCountScan` emission on it.\n- The fast paths are preserved for root / owner sessions via `should_check_perms_for_view`.\n\nVersions 3.1.0 and later are not affected.\n\n### Workarounds\n\nUsers unable to patch are advised to consider the following workarounds:\n\n- Avoid `DEFINE INDEX` on fields whose values are protected by field-level SELECT permissions. The class of attack is specific to the indexed fast paths.\n- Restrict the ability of record users to issue arbitrary `SELECT count() \u2026 GROUP ALL` queries against tables containing field-protected columns.\n- Use namespace / database isolation as the primary boundary where feasible.",
"id": "GHSA-c8jx-96c9-8xrp",
"modified": "2026-07-01T20:22:06Z",
"published": "2026-07-01T20:22:06Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/surrealdb/surrealdb/security/advisories/GHSA-c8jx-96c9-8xrp"
},
{
"type": "WEB",
"url": "https://github.com/surrealdb/surrealdb/pull/240"
},
{
"type": "WEB",
"url": "https://github.com/surrealdb/surrealdb/commit/0c6dd021bb55b32a78a553c72bb9c0cdd414825f"
},
{
"type": "PACKAGE",
"url": "https://github.com/surrealdb/surrealdb"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "SurrealDB: Field-level SELECT permissions bypassed via indexed COUNT fast paths"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.