db-security-access (M10)
Security is a Design & Integrity (axis design) concern: a schema can be perfectly normalized and
still leak every row. This module checks who can read/write data and whether the data itself is
protected at rest and in flight. RLS-off on a relied-on multi-tenant/Supabase table and plaintext
secrets/SQL-injection are severity-5 caps.
What it checks
- Row-Level Security (RLS) — on Postgres/Supabase, is RLS enabled on tables that hold per-tenant
or per-user rows and are reached by a non-superuser/
anon/authenticated role? RLS off on such a
table is severity:5, fail. (Tenant isolation logic lives in M9; M10 owns the on/off state.)
- PII exposure — columns whose names/types imply personal data (email, ssn, phone, dob, address,
card/pan) stored without encryption/tokenization, or logged.
- Plaintext secrets in schema — passwords, API keys, tokens stored as
text/varchar with no
hashing note, or literal credentials embedded in DDL/migrations/defaults. severity:5.
- Encryption in transit — connection config forcing
sslmode=disable (or no TLS on a remote
host) is severity:4. Encryption at-rest absence is flagged where statically visible.
- SQL injection — raw string concatenation / f-strings / template literals building SQL with
user input (visible in ORM source or migration helpers).
severity:5, design.
Score / axis
Feeds design only (category Seguridad, relational weight 14 shared with M9/M20/M21; analogous
Seguridad category in every NoSQL profile).
Tier-0 (static)
Parse DDL/migrations/ORM source and connection config: detect text-typed secret columns, literal
credentials (cross-checked against redactSecrets()), sslmode=disable, raw-concat SQL, and
PII-named columns. RLS enablement (ALTER TABLE … ENABLE ROW LEVEL SECURITY) is detectable in
declarative DDL; when RLS state cannot be confirmed from files it is needs_api (never a silent pass).
Tier-1 (verification query, Postgres)
SELECT c.relname,
c.relrowsecurity AS rls_enabled,
c.relforcerowsecurity AS rls_forced,
(SELECT count(*) FROM pg_policies p WHERE p.tablename = c.relname) AS policy_count
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relkind = 'r';
Method schema_introspect/constraint_check. A table with rls_enabled = false that holds tenant/
user rows confirms M10.rls.* as established and capping. Runtime-only checks (actual at-rest
encryption, live TLS negotiation) are needs_api at Tier-0.
Findings
Emit findings per schema/finding.schema.json. Examples:
M10.tenants.rls_disabled — RLS off on a per-tenant table (severity:5, fail, axis design,
confidence established at Tier-1 / directional from DDL, fixable: proposed).
M10.users.password_plaintext — secret stored as text with no hash (severity:5, fail,
established, fixable: advisory).
M10.conn.sslmode_disable — sslmode=disable on a remote host (severity:4, warn,
fixable: proposed).
M10.repo.sql_string_concat — user input concatenated into SQL (severity:5, fail, directional
from source, fixable: advisory).
Each finding: evidence.observed quotes the DDL/connection line/query verbatim with secrets
redacted; verification.reproduce is the catalog query above (referencing $DATABASE_URL) or a
grep for the offending pattern; expected_impact is banded + confidence-tagged (no naked %).
Honesty
- "PII present" is not automatically a fail — flag unprotected PII; many columns are legitimately
plaintext. Scope the recommendation to encryption/tokenization, not deletion.
- At-rest encryption is usually a platform setting (cloud KMS/volume) invisible in files: report
needs_api, never assert it is missing.
- A
directional source-only RLS/injection signal never raises the severity-5 cap — confirm via
Tier-1 or generated DDL first.
1---2name: db-security-access3description: Audit access control and data protection — Row-Level Security state, PII handling, encryption at-rest and in-transit (TLS / sslmode), and SQL-injection exposure from raw concatenation. Module M10. Feeds the Design & Integrity score.4---56# db-security-access (M10)78Security is a **Design & Integrity** (axis `design`) concern: a schema can be perfectly normalized and9still leak every row. This module checks who *can* read/write data and whether the data itself is10protected at rest and in flight. RLS-off on a relied-on multi-tenant/Supabase table and plaintext11secrets/SQL-injection are severity-5 caps.1213## What it checks14151. **Row-Level Security (RLS)** — on Postgres/Supabase, is RLS enabled on tables that hold per-tenant16 or per-user rows and are reached by a non-superuser/`anon`/`authenticated` role? RLS off on such a17 table is `severity:5`, `fail`. (Tenant *isolation* logic lives in M9; M10 owns the on/off state.)182. **PII exposure** — columns whose names/types imply personal data (email, ssn, phone, dob, address,19 `card`/`pan`) stored without encryption/tokenization, or logged.203. **Plaintext secrets in schema** — passwords, API keys, tokens stored as `text`/`varchar` with no21 hashing note, or literal credentials embedded in DDL/migrations/defaults. `severity:5`.224. **Encryption in transit** — connection config forcing `sslmode=disable` (or no TLS on a remote23 host) is `severity:4`. Encryption at-rest absence is flagged where statically visible.245. **SQL injection** — raw string concatenation / f-strings / template literals building SQL with25 user input (visible in ORM source or migration helpers). `severity:5`, `design`.2627## Score / axis2829Feeds **design** only (category *Seguridad*, relational weight 14 shared with M9/M20/M21; analogous30*Seguridad* category in every NoSQL profile).3132## Tier-0 (static)3334Parse DDL/migrations/ORM source and connection config: detect `text`-typed secret columns, literal35credentials (cross-checked against `redactSecrets()`), `sslmode=disable`, raw-concat SQL, and36PII-named columns. RLS *enablement* (`ALTER TABLE … ENABLE ROW LEVEL SECURITY`) is detectable in37declarative DDL; when RLS state cannot be confirmed from files it is `needs_api` (never a silent pass).3839## Tier-1 (verification query, Postgres)4041```sql42SELECT c.relname,43 c.relrowsecurity AS rls_enabled,44 c.relforcerowsecurity AS rls_forced,45 (SELECT count(*) FROM pg_policies p WHERE p.tablename = c.relname) AS policy_count46FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace47WHERE n.nspname = 'public' AND c.relkind = 'r';48```49Method `schema_introspect`/`constraint_check`. A table with `rls_enabled = false` that holds tenant/50user rows confirms `M10.rls.*` as `established` and capping. Runtime-only checks (actual at-rest51encryption, live TLS negotiation) are `needs_api` at Tier-0.5253## Findings5455Emit findings per `schema/finding.schema.json`. Examples:56- `M10.tenants.rls_disabled` — RLS off on a per-tenant table (`severity:5`, `fail`, axis `design`,57 confidence `established` at Tier-1 / `directional` from DDL, `fixable: proposed`).58- `M10.users.password_plaintext` — secret stored as `text` with no hash (`severity:5`, `fail`,59 `established`, `fixable: advisory`).60- `M10.conn.sslmode_disable` — `sslmode=disable` on a remote host (`severity:4`, `warn`,61 `fixable: proposed`).62- `M10.repo.sql_string_concat` — user input concatenated into SQL (`severity:5`, `fail`, `directional`63 from source, `fixable: advisory`).6465Each finding: `evidence.observed` quotes the DDL/connection line/query **verbatim with secrets66redacted**; `verification.reproduce` is the catalog query above (referencing `$DATABASE_URL`) or a67`grep` for the offending pattern; `expected_impact` is banded + confidence-tagged (no naked %).6869## Honesty7071- "PII present" is not automatically a fail — flag *unprotected* PII; many columns are legitimately72 plaintext. Scope the recommendation to encryption/tokenization, not deletion.73- At-rest encryption is usually a **platform** setting (cloud KMS/volume) invisible in files: report74 `needs_api`, never assert it is missing.75- A `directional` source-only RLS/injection signal **never raises the severity-5 cap** — confirm via76 Tier-1 or generated DDL first.