Filed unassigned by the #13496 dev while auditing null-member guarding across the security $in construction paths (that card's A2 completeness check). Recording only — no severity asserted, routing is triage's.
Measured
packages/plugins/plugin-sharing/src/sharing-service.ts builds the record-share half of both the read filter and the bulk-write filter the same way, at two sites (buildReadFilter, ~line 419, and the write path, ~line 497):
const grantedIds: string[] = Array.isArray(grants)
? grants.map((g: any) => String(g.record_id)).filter(Boolean)
: [];
.filter(Boolean) reads as "drop rows whose record_id is nullish". It cannot do that: String(null) is 'null' and String(undefined) is 'undefined' — both truthy. The only input the filter can drop is the empty string. So the guard is dead for exactly the case its spelling advertises, and a sys_record_share row with a null record_id contributes the literal string 'null' to a security predicate: { id: { $in: [..., 'null'] } }.
Direction and blast radius — small, which is why this is recorded and not escalated
- The emitted member is a bogus id, so it matches no row on any backend: the grant is silently DROPPED rather than the scope widened.
- Both sites are positive polarity (a hand-built
$or branch, never negated), so the "matches nothing" reading does not invert into "matches everything".
- It requires a
sys_record_share row with a null record_id — already-corrupt data.
The defect is therefore the dead guard, not a live bypass: a reader — or a future null-hardening sweep looking for which security paths are already covered — will read .filter(Boolean) as covering the nullish case when it provably cannot. The sibling paths audited alongside it all use a guard that does fire.
What this does NOT claim
I did not demonstrate a sys_record_share row with a null record_id. I demonstrated that the guard standing in front of that case cannot fire.
Related
#13496 (the null-member audit that surfaced this). The guarded siblings it was compared against, all of which strip correctly: core/src/security/resolve-authz-context.ts (org_user_ids, accessible_org_ids), plugin-security/src/security-plugin.ts (controlled-by-parent masterIds), objectql/src/engine.ts, objectql/src/search-filter.ts.
Generated by Claude Code
Filed unassigned by the #13496 dev while auditing null-member guarding across the security
$inconstruction paths (that card's A2 completeness check). Recording only — no severity asserted, routing is triage's.Measured
packages/plugins/plugin-sharing/src/sharing-service.tsbuilds the record-share half of both the read filter and the bulk-write filter the same way, at two sites (buildReadFilter, ~line 419, and the write path, ~line 497):.filter(Boolean)reads as "drop rows whoserecord_idis nullish". It cannot do that:String(null)is'null'andString(undefined)is'undefined'— both truthy. The only input the filter can drop is the empty string. So the guard is dead for exactly the case its spelling advertises, and asys_record_sharerow with a nullrecord_idcontributes the literal string'null'to a security predicate:{ id: { $in: [..., 'null'] } }.Direction and blast radius — small, which is why this is recorded and not escalated
$orbranch, never negated), so the "matches nothing" reading does not invert into "matches everything".sys_record_sharerow with a nullrecord_id— already-corrupt data.The defect is therefore the dead guard, not a live bypass: a reader — or a future null-hardening sweep looking for which security paths are already covered — will read
.filter(Boolean)as covering the nullish case when it provably cannot. The sibling paths audited alongside it all use a guard that does fire.What this does NOT claim
I did not demonstrate a
sys_record_sharerow with a nullrecord_id. I demonstrated that the guard standing in front of that case cannot fire.Related
#13496 (the null-member audit that surfaced this). The guarded siblings it was compared against, all of which strip correctly:
core/src/security/resolve-authz-context.ts(org_user_ids,accessible_org_ids),plugin-security/src/security-plugin.ts(controlled-by-parentmasterIds),objectql/src/engine.ts,objectql/src/search-filter.ts.Generated by Claude Code