Security Notice: Incomplete Row-Level Security Coverage Across Domain Tables
Security Notice: Incomplete Row-Level Security Coverage Across Domain Tables
Version: 1.0.12
Severity: 🔴 Critical
Category: Enterprise Infrastructure / Multi-Tenant Security
Overview
A critical gap has been identified in the platform's Row-Level Security (RLS) implementation. Database-level RLS policies are currently enforced on only 6 of the 30+ tables in the system. The remaining domain tables — which hold the core Irish block management data — depend exclusively on application-level tenant filtering. This does not provide sufficient isolation in a multi-tenant SaaS environment.
Background: RLS in a Multi-Tenant Platform
The platform serves multiple Owners' Management Companies (OMCs) and property management agents, each operating as a separate organisation (orgId). Strict data isolation between tenants is a fundamental security and compliance requirement.
Row-Level Security (RLS) enforces this isolation at the PostgreSQL database level, meaning that even if application code has a bug or is bypassed, the database itself will refuse to return rows belonging to another tenant.
What Was Found
The rls.ts file defines RLS policies for the following 6 tables only:
| Table | RLS Enforced |
|---|---|
org_members | ✅ Yes |
subscriptions | ✅ Yes |
usage_events | ✅ Yes |
api_keys | ✅ Yes |
audit_log | ✅ Yes |
webhooks | ✅ Yes |
The following domain tables (and more) have no database-level RLS policies:
| Table | RLS Enforced |
|---|---|
developments | ❌ No |
units | ❌ No |
owners | ❌ No |
serviceChargeDemands | ❌ No |
complianceObligations | ❌ No |
maintenanceRequests | ❌ No |
| (30+ additional domain tables) | ❌ No |
How the Gap Arises
For unprotected tables, tenant isolation is handled entirely within the application layer using a filter of the form:
and(eq(table.orgId, ctx.orgId))
This pattern is applied consistently in query builders throughout the codebase. However, it has two critical weaknesses:
- SQL Injection: A successful injection attack can craft queries that bypass or remove the
orgIdfilter, exposing rows from other tenants. - Application Bugs: A logic error, missing filter, or code path that skips the
orgIdcheck can inadvertently return or mutate another tenant's data.
Database-level RLS eliminates both risks because the filter is enforced unconditionally by PostgreSQL, regardless of how the query was constructed.
Risk Assessment
| Factor | Detail |
|---|---|
| Confidentiality | Cross-tenant leakage of owner PII, financial records, and compliance data is possible |
| Integrity | Writes or mutations could potentially affect another tenant's records |
| Regulatory | Irish data protection obligations (GDPR) apply to owner and unit holder data |
| Blast radius | All 30+ domain tables and every OMC organisation on the platform |
Required Remediation
RLS policies must be extended to cover all domain tables. The existing policies in rls.ts for the 6 covered tables provide a reference pattern to follow. Key steps:
- Audit all tables — enumerate every table in the schema that stores tenant-scoped data.
- Define RLS policies in
rls.ts— addSELECT,INSERT,UPDATE, andDELETEpolicies for each table, scoped tocurrent_setting('app.org_id')or the equivalent session variable. - Enable RLS on each table — ensure
ALTER TABLE ... ENABLE ROW LEVEL SECURITYis applied. - Retain application-level filtering —
orgIdfilters in query builders should remain as a defence-in-depth layer. - Test cross-tenant isolation — add integration tests that verify a session for Org A cannot read or write rows belonging to Org B for every domain table.
Interim Mitigations
While full RLS coverage is being implemented, the following measures reduce (but do not eliminate) risk:
- Conduct a thorough audit of all query builders to confirm
orgIdfilters are applied consistently. - Review and harden input validation and parameterisation to reduce SQL injection surface area.
- Enable database query logging and alerting for queries that return an unexpectedly high number of rows across apparent tenant boundaries.
- Restrict direct database access to application service accounts only; revoke any broader credentials.
References
- PostgreSQL Row Security Policies Documentation
- Irish GDPR obligations under the Data Protection Act 2018
- OMC data classification policy (internal)