12 August 2026 · Actor 1099:claude-cowork-soc2 · Session fad23e8d
The Supabase security advisors were skipped in earlier passes because the raw output is ~96–146 KB per project. Read in full this time (432 KB across four projects, in isolation), cross-checked against the live catalog, and triaged. Three live exploitable findings. All three fixed and verified.
| Lint | FedM8 | NodeDough | CipherDeck | jbnx |
|---|---|---|---|---|
authenticated_security_definer_function_executable | 74 | 114 | 14 | 23 |
rls_enabled_no_policy | 13 | 19 | 37 | 65 |
security_definer_view | 0 | 0 | 0 | 48 |
anon_security_definer_function_executable | 6 | 5 | 5 | 3 |
function_search_path_mutable | 0 | 0 | 0 | 3 |
extension_in_public | 1 | 2 | 0 | 0 |
rls_disabled_in_public: zero everywhere — relrowsecurity = true on every table in every API-exposed schema, verified in the catalog.
pf_project_audits, pf_delivery and pf_requests are SECURITY DEFINER views over the portfolio schema with SELECT granted to authenticated and no authorization predicate. A non-invoker view runs with the owner's rights, so any signed-in account — including one that is not a portfolio member — read 100% of rows: every project's audit posture and evidence URLs, delivery and WIP metrics, and inbound requests including submitted_by, contact, body and triage_note.
The other 43 pf_ views already carry portfolio.is_member_reader(). These three were missed.
Fix: added the same predicate. Not security_invoker = on — the browser role has no grant on the portfolio schema, which is the entire reason these views exist, so invoker mode would have returned zero rows for legitimate members and blanked the portal.
Verified empirically across all 41 populated pf_ views, impersonating a real auth.users id that is not in portfolio.members:
| View | Owner rows | Non-member, before | Non-member, after |
|---|---|---|---|
pf_project_audits | 154 | 154 | 0 |
pf_delivery | 14 | 14 | 0 |
pf_requests | 6 | 6 | 0 |
| all 38 others | various | 0 | 0 |
Owner context still returns 154 rows — the portal is not broken.
truepublic.cd_is_subscribed(uuid) had the literal body select true;. It is the entire premium check in the WITH CHECK of two policies — inventory_sessions."inv insert own, premium only" and price_alerts."price_alerts own insert, premium only" — so every signed-in user, paying or not, could write premium rows.
Fix: implemented against the real entitlement source, profiles.subscription_status with the comped_until escape hatch, treating past_due as entitled to match FedM8's my_plan() convention.
Deliberately not implemented against public.subscribers — despite the name, that is newsletter email capture (source='cipherdeck.com'), and keying a paywall to it would entitle anyone who ever submitted an email address. Checking that before writing the predicate was the difference between a fix and an outage.
Verified: unconditional true is gone; a random UUID is denied; anon can no longer execute it. 12 of 13 profiles still resolve as entitled — correct, not a regression: they hold an explicit comped_until = 2099-01-01 grant. Only the single profile with no comp and no subscription is denied.
Business observation, not a security finding: 10 users have an open-ended comp to 2099 with no plan and subscription_status = 'none'. If that was a beta grant, it never expires and no amount of paywall correctness will bill them.
cd_uptime_ping() was executable by anon. Every unauthenticated RPC call made an outbound HTTP GET to app.cipherdeck.com/api/health and inserted a row into uptime_checks — 2,080 rows since 5 August. Anyone could use your own database to generate traffic against your app and grow a table without bound.
Fix: revoked from public, anon and authenticated. service_role retains EXECUTE, so the legitimate scheduled ping is unaffected — verified.
anon holds SELECT on 37 tables including vet_profiles, subscriptions, pp_vault_entries. Not reachable: every policy is auth.uid() = user_id, which is NULL for anon. Veteran PII is closed. Dead grants worth revoking so a future permissive policy cannot silently open them.
exist anywhere in the schema.
anon has schema USAGE on nguy plus SELECT+INSERT on 12 tables including google_tokens, webauthn_credentials, sessions. RLS on, no anon policies → denied. The highest-value dead grant in the estate. Note the portal Supabase project is shared with nguy and support, so scope any revoke carefully.
pf_invite_redeem brute-force brake is inert. The failure-path insert into invite_attempts and bad_tries increment are both rolled back by the raise exception that follows, so the counter is permanently 0. No impact today — only 2 invites exist and redemption needs the exact invited email — but the rate limiter does not function.
validate_invite_code: unauthenticated, unrated, unlogged existence oracle.Table is empty today; live the moment household invites are issued.
authenticated_security_definer_function_executable (74/114/23/14) fires on the entire intended RPC API. The admin_* families each open with an internal guard, made fail-closed on 12 Aug. Not actionable as a class — and the one genuinely broken guard, cd_is_subscribed, is fixed above.
rls_enabled_no_policy (13/19/37/65) is INFO and describes the desired state forservice-role-only tables: RLS on with no policies denies everyone. Do not "remediate" by adding policies — that would open them.
security_definer_view hits carry an explicit member/ops predicate and were verified closed. Only the 3 above were real (plus pf_briefs, already gated by is_owner()).
function_search_path_mutable (3, all agent_ops) — all prosecdef = false with no anon/authenticated EXECUTE. Mutable search_path is an escalation vector only on SECURITY DEFINER functions.
get_advisors returns database linter output only. There are zero auth_otp_long_expiry, auth_leaked_password_protection or insufficient_mfa_options entries in any payload — confirmed by grep over the full text, not inferred from absence. OTP expiry, leaked-password protection and MFA enrolment are unassessed and cannot be read over SQL. They need the dashboard Auth settings or the Management API.