Skip to content

plugin-sharing: the .filter(Boolean) guard on grantedIds cannot fire — String() has already made every value truthy #13551

Description

@claude

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions