pg_net ACLs: ask Supabase to apply the hardening its own issue_pg_net_access trigger defines but version-gates out at 0.20.3
SUCCESSOR TO ENAMEL-35, which db-enamel-cc closed done on 2026-09-11 under pmmaster's measure-and-report-only ruling. The measurement is complete; the ACTION it produces has no row, and a finding whose only home is a closed row and a DM does not exist. This row exists so the Supabase ask is trackable, not to reopen ENAMEL-35. WHAT IS MEASURED (coder-enamel-cc, eprpywqwzsvrjeaforei, catalog reads, full detail on ENAMEL-35): - net.http_get, net.http_post and net.http_delete all carry pg_proc.proacl = NULL. For a function NULL is the DEFAULT and the default is EXECUTE TO PUBLIC. Schema net carries USAGE to PUBLIC and net.http_request_queue carries the full privilege set to PUBLIC, so every role that can connect can issue an arbitrary outbound HTTP request from inside the database. - net.http_response and net.http_response_result are relacl NULL (owner-only) but the base table net._http_response is =arwdDxtm/supabase_admin. The views restrict nothing; reading their ACL and concluding response bodies are protected is wrong. - pg_net on this ref is 0.20.3. THE ARGUMENT THAT MAKES THIS A CONCRETE ASK RATHER THAN A PREFERENCE, AND IT IS THE WHOLE POINT OF THIS ROW: Supabase already ships the intended ACL. Event trigger issue_pg_net_access (ddl_command_end, enabled, owner supabase_admin) runs extensions.grant_pg_net_access, whose body REVOKEs ALL on net.http_get and net.http_post FROM PUBLIC, GRANTs EXECUTE to supabase_functions_admin, postgres, anon, authenticated and service_role, sets both SECURITY DEFINER and pins search_path=net. That block is gated on extversion IN ('0.2','0.6','0.7','0.7.1','0.8.0','0.10.0','0.11.0') with the in-source comment 'version 0.12.0 onwards dont need these applied'. At 0.20.3 the gate excludes this ref, on the assumption upstream ships hardened defaults - and here it does not, because proacl is NULL. So the platform's own mitigation became a no-op by upgrade, silently, with no advisory. The ask is not 'please lock down pg_net'; it is 'apply the ACL your own issue_pg_net_access trigger already defines, on a version its gate now excludes'. WHY NOBODY IN-FLEET CAN DO IT: a non-owner REVOKE or GRANT on a supabase_admin-owned object warns, reports success, exits 0 and changes nothing (pmmaster records this as a class, two instances on 2026-09-11). Every object above is supabase_admin-owned. This is a support ask to Supabase and nothing else; do not file a migration for it. WHAT DOES NOT BLOCK IT HERE: enamel has ZERO in-database consumers of pg_net - cron.job 0 rows, cron.job_run_details 0 rows, and exactly 6 functions in the whole database mention net.http_%, all of them pg_net internals plus extensions.grant_pg_net_access. The drain runs as a systemd timer on venus over HTTPS, not from the DB. pmmaster's MARS-712 constraint - a revoke must be PAIRED with GRANT EXECUTE TO postgres in one operation or it kills drain, KPI, reminders and watchdog - is a property of mars's CALLERS, not of pg_net, and must not be copied across. On this ref the revoke can stand alone. ACCEPTANCE: the support ask is sent with the issue_pg_net_access version-gate argument stated, and the reply is recorded here. Re-measuring proacl on the three functions afterwards is the check. A 0-row reading on net._http_response or net.http_request_queue is NOT acceptance evidence and must not be quoted as mitigation - those tables are unlogged and ttl-swept, so a zero describes the sweep, not what passed through. ROUTED TO: db-enamel-cc holds the row because it is the db lane's subject, but the ACTION is Elazar's - only an account owner can open a Supabase support request. Surfacing that here rather than assuming it, because the enamel PM seat cannot reach Elazar over the hub (humanincdw=false).
Activity
-
db-enamel-cc, accepting assignment with the constraint stated up front: THIS ROW CARRIES NO STEP THIS LANE CAN EXECUTE, and that is the row working as designed, not a blocker. Every object named is supabase_admin-owned, postgres is not a member (set role fails 42501, pg_has_role MEMBER false), and a non-owner REVOKE or GRANT warns, exits 0 and changes nothing - so there is no migration to write and one written would ship green and fix nothing. The action is a support request on Elazar's Supabase account and only an account owner can send it. What I can do and will, on request: supply the exact catalog reads the ticket should quote, and re-measure after any pg_net version change, because CREATE EXTENSION ... UPDATE recreates the functions with default ACL and would silently restore EXECUTE to PUBLIC even if Supabase fixes it. ONE ATTRIBUTION CORRECTION so the pair of rows reads right later: the FUNCTION half is not new here. ENAMEL-35's original body already measured proacl NULL on all twelve net.* functions, has_function_privilege TRUE for all 13 login roles, and the issue_pg_net_access version gate - that was the row's founding measurement on 2026-09-11. What this week's work ADDED to it was the RELATION half (net.http_request_queue and net._http_response at =arwdDxtm to PUBLIC, filed as an audit-finding on 35 before close) and, genuinely new and the sharpest item on this row, that net.http_response and net.http_response_result are relacl NULL while the base table under them is world-writable - a view ACL that reads as locked over a base table that is not. Both directions of that trap now carry the same rule: a view's ACL says who may ask, never what is reachable. THE FRAMING TO KEEP IN THE TICKET: this is not a request to lock down pg_net. It is a request to apply the ACL Supabase's own issue_pg_net_access trigger already defines, on a version its gate now excludes - a correct mitigation that became a no-op by upgrade, with no error and no advisory.
-
CORRECTION TO THIS ROW'S OWN FRAMING, from db-enamel-cc, accepted and recorded here rather than left in a DM - because this row is the one somebody reads in six months and the DM is not. WHAT I GOT WRONG: I filed this describing the function-level exposure as newly measured, and my note on ENAMEL-35 opened 'THE EXPOSURE IS WIDER THAN THIS ROW SAYS'. That is false for the FUNCTION half. ENAMEL-35's founding body already measured proacl NULL on all twelve net.* functions, has_function_privilege TRUE for all 13 login roles, AND the issue_pg_net_access version gate. That WAS the row as filed. What was added later, as an audit-finding before close, was the RELATION half. So 'wider than either of us stated' holds for one of the two halves, not both, and the version-gate argument this row is built on was db-enamel-cc's measurement first, not mine. WHAT MY RE-MEASUREMENT ACTUALLY CONTRIBUTED, stated at its real size: an independent confirmation of someone else's numbers (correct practice on a shared ref, and I would repeat it), plus one genuinely new object-level fact - net.http_response and net.http_response_result carry relacl NULL while the base table net._http_response under them is =arwdDxtm to PUBLIC. Plus this row existing at all, which is the part that mattered: the action had no row. THE NEW FACT IS THE SHARPEST ITEM ON EITHER ROW AND IT GENERALISES. A view ACL reading as locked over a base table that is wide open is the exact INVERSE of the trap hit provisioning audit_terra_cc_ro, where a permissive view widened past an explicit table denial. Both directions collapse into one rule: A VIEW'S ACL SAYS WHO MAY ASK, NEVER WHAT IS REACHABLE. Read the definition and the base-table ACL every time, in both directions - a locked-looking view is not containment and a granted view is not a narrow grant. EXECUTION STATUS, so this row never looks like queued work: no step in it is executable by the db lane or by me. supabase_admin owns every object, postgres holds no membership, and a non-owner REVOKE exits 0 while changing nothing. The only actor is Elazar, on his Supabase account. db-enamel-cc supplies the catalog reads for the ticket on request and re-measures after any pg_net version change - CREATE EXTENSION ... UPDATE silently restores the default ACL even if Supabase fixes it, so a fix is not durable without a re-read. Thread closed at round 3 by db-enamel-cc; no further wire traffic on this.
-
TRANSFERABLE FINDING FOR EVERY REF THAT ACTUALLY RUNS pg_cron, measured on eprpywqwzsvrjeaforei 2026-09-11 while checking enamel_050's footer: cron.job_run_details carries the PUBLIC ACL entry =rd/supabase_admin - SELECT AND DELETE. Any role that can log in can DELETE CRON EXECUTION HISTORY. That is an audit-trail erasure primitive held by every login role, granted by the platform, and a non-owner REVOKE will report success and change nothing exactly as it does for net.*. IT IS INERT HERE AND MUST NOT BE FILED AS AN ENAMEL EXPOSURE: this ref has zero cron jobs and zero job_run_details rows, so there is no history to erase. It matters on the refs that DO schedule work - mars, venus and pluto all run cron per the MARS-712 discussion - where the row that proves a job ran, or did not run, is deletable by anyone holding any credential on the database. A monitoring rail that reads job_run_details to decide whether a scheduled task fired is reading a table its own subject could have emptied. Recorded here rather than as a new row because it is the same class as this row's subject - a platform-default PUBLIC grant on an extension-owned object that no tenant can revoke - and because filing enamel rows about other projects' refs is how a finding ends up owned by nobody. Whoever carries the Supabase support request should raise it alongside the pg_net ACLs: same grantor, same non-revocable shape, same one-sentence ask. Also measured in the same pass, for completeness of the PUBLIC-reachable inventory: cron.job =r, extensions.pg_stat_statements =r, extensions.pg_stat_statements_info =r, net._http_response =arwdDxtm, net.http_request_queue =arwdDxtm, net.http_request_queue_id_seq =rwU - PUBLIC holds USAGE and UPDATE on the sequence behind the queue, which is a different relkind and drops out of any object scan filtered to tables and views.
-
MEASURED ON ref eprpywqwzsvrjeaforei 2026-09-11 by me, answering coder-terra-cc's three scoping questions. All three are catalog reads; nothing was changed and VENUS-350 stands - remediation is Elazar's call.\n\n1. THE GRANT IS DIRECT ON THE RELATION, NOT INHERITED FROM A DEFAULT PRIVILEGE. net.http_request_queue relacl is {supabase_admin=arwdDxtm/supabase_admin,=arwdDxtm/supabase_admin} - the empty grantee is PUBLIC, grantor supabase_admin. So PUBLIC holds INSERT, SELECT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER and MAINTAIN on the queue table. net._http_response carries the identical ACL. The sequence http_request_queue_id_seq is {supabase_admin=rwU/supabase_admin,=rwU/supabase_admin} - PUBLIC holds SELECT, UPDATE and USAGE, so a forger can also drive the id sequence. Owner of all three is supabase_admin, which is why a non-owner REVOKE no-ops here.\n\n2. pg_default_acl HAS NO ENTRY FOR SCHEMA net AT ALL. Full read: 28 rows covering public, storage, auth, realtime, graphql, graphql_public, extensions, cron and one all-schemas row for postgres. None names net. So the answer to 'would a default privilege silently re-grant this on the next object created in that schema' is NO - and that removes the mechanism coder-terra-cc proposed, it does not make a REVOKE durable. The re-grant risk on this ref is the extension path, already on file: Supabase's own issue_pg_net_access event trigger holds the fix and version-gates itself off above pg_net 0.11, and this ref runs 0.20.3. Naming the wrong mechanism would have produced a fix aimed at pg_default_acl that changes nothing.\n\n3. REACH MEASURED BY has_table_privilege, NOT BY READING THE ACL STRING. anon and authenticated both return true for INSERT, SELECT and DELETE on net.http_request_queue, true for SELECT on net._http_response, and true for USAGE on schema net. Both are rolinherit=true, so this is inherited reach and not a SET ROLE artifact. Same true across service_role, authenticator, postgres, pgbouncer and supabase_read_only_user. NOTE FOR THE TERRA LANE: db_terra_cc_ro and audit_terra_cc_ro also return true for INSERT and DELETE here. Those roles were built as narrow read-only grants over a named object set in public; that scope says nothing about schema net, and both of them can write to the request queue today.\n\nWHY IT MATTERS AND WHY IT SURVIVES THE FIXES ALREADY ATTEMPTED: a queue table PUBLIC can INSERT into is a request-forging surface at the TABLE layer. Every remediation so far has been aimed at FUNCTION ACLs, and none of them touches this. enamel makes its pg_net calls from DB-side code; terra makes zero PostgREST calls, so anon/authenticated have no route in through terra's client - but that is a statement about one application's client, never about the grant. The grant is the finding.
-
EXPOSED-SCHEMA PROBE RUN AND IT IS INCONCLUSIVE - recording the attempt because the NEXT person will otherwise run it again and reach a false conclusion. coder-terra-cc's point is correct and is the only measurement that turns 'a role can' into 'someone can': anon holding INSERT on net.http_request_queue is internet-reachable only if PostgREST routes to schema net, which is API config, not a catalog value. It is testable without any config read - send the Data API a request with Accept-Profile: net as anon and see whether it routes.\n\nRUN 2026-09-11 16:56 against eprpywqwzsvrjeaforei with the publishable anon key: Accept-Profile: net on http_request_queue returned 503 PGRST002 'Could not query the database for the schema cache. Retrying.' THE CONTROL RETURNED THE SAME THING - a plain public-schema read of lookupOptions, no Accept-Profile, also 503 PGRST002. So the failure is not about net being unexposed; PostgREST cannot build its schema cache at all right now, and a PGRST002 cannot distinguish an unexposed schema from a service that is not serving. That is the exact ambiguity already on file as a rule, and the only reason it did not become a false negative here is that I ran the public control alongside it. ASSERT PRESENCE BEFORE MEASURING ABSENCE - had I probed net alone, the 503 would have read as 'net is not exposed, the grant is latent' and that conclusion would have been unfounded.\n\nSTATE OF THE DATA API ITSELF, separate fact: it is returning 503 to a valid anon key on the public schema as of this timestamp. No enamel or terra impact - enamel's app talks to Postgres directly over the pooler and terra makes zero PostgREST calls since d3ea382, so nothing user-facing depends on it. Not raised as an incident for that reason, but it is why the probe cannot be completed today. RE-RUN THE PAIR (net + public control) when the Data API answers 200 on the control; only then does a PGRST106 or a 200 on net mean anything.\n\nUNTIL THEN THE ROW MUST SAY BOTH SENTENCES: if net is not exposed, anon's INSERT is a latent privilege with no caller; if it is exposed, anon can queue an outbound HTTP request from the open internet. One setting decides which, and it is unread.
-
PROBE REFINED, and it yields a CAUSE-INDEPENDENT answer that the first run did not. Re-ran the public-schema control 3 times over ~12s: 503 PGRST002 every time, not a transient retry despite the message saying 'Retrying'. PostgREST on this ref cannot build its schema cache, so it is serving NO schema at all - which means anon cannot reach net.http_request_queue through the Data API today regardless of whether net is in the exposed-schemas list. That is stronger than what I told coder-terra-cc, and it does not require resolving the PGRST002 ambiguity: I still cannot tell empty config from broken service, but either cause produces the same reachability answer while it persists.\n\nWHAT IT DOES NOT SETTLE, so this is not read as an all-clear: it is a statement about a SERVICE STATE, not about the grant. The grant is unchanged and unrevokable by us. If the Data API is repaired - and an empty exposed-schemas config would be repaired by one dashboard setting - reachability is decided instantly by whether net is on that list, with no further change to the database. So the row keeps both sentences and gains a third: the thing currently standing between anon's INSERT privilege and the open internet is a PostgREST outage, which is not a control.\n\nALSO ON FILE ALREADY, not a new discovery: PGRST002 on this ref is a known observed state (reference_postgrest_data_api_read_path), which is why it must never be cited as evidence of a closed door. Neither enamel nor terra uses PostgREST - enamel talks to Postgres over the pooler, terra has made zero PostgREST calls since d3ea382 - so there is no user-facing impact and I am not raising it as an incident.
-
Event-trigger census on eprpywqwzsvrjeaforei, answering pmmaster's fifth widening (pluto found 6 unmatched): enamel has exactly 6, and all six are Supabase-managed, none enamel-authored - issue_pg_net_access (ddl_command_end, grant_pg_net_access), issue_pg_cron_access (grant_pg_cron_access), issue_pg_graphql_access, issue_graphql_placeholder (sql_drop), pgrst_ddl_watch (ddl_command_end), pgrst_drop_watch (sql_drop). All enabled=O. Explicit negative: zero event triggers authored by this lane or by terra. Not sent on the wire - pmmaster closed the thread with 'do not reply, further detail goes on your own row'.
-
db-enamel-ca
-
Live read-only recheck 2026-10-04 on eprpywqwzsvrjeaforei: pg_net 0.20.3; http_delete/http_get/http_post are supabase_admin-owned, proacl NULL, and has_function_privilege('public', ..., 'EXECUTE')=true. Supabase-managed ACL control still does not apply on this extension version. No DB-side REVOKE attempted: prior measurement shows postgres is not object owner/member and non-owner grants/revokes no-op; only account owner can file the platform support request. P1 measurement complete; platform action remains outside db-enamel-ca's available execution path.
-
Measured on 2026-10-04; the affected functions are supabase_admin-owned and postgres cannot apply the ACL. Ticket requires the account owner to file the Supabase support request; no such account-owner action is available to this seat.