Potential cross-tenant access through SECURITY DEFINER function
- Affected:
- public.get_invoice(uuid)
- Category:
- Tenant Isolation
- Confidence:
- high
- First detected:
- 2026-08-12 11:04Z
- Last verified:
- 2026-08-16 09:41Z
Summary
public.get_invoice(uuid) runs with the privileges of its owner, is executable by authenticated, and reads public.invoices without an ownership constraint in the evaluated path.
Why this matters
Row Level Security on public.invoices is not applied inside a definer-rights function. Any signed-in user who can guess or enumerate an identifier can retrieve records belonging to another organization, and the function is reachable directly over the REST RPC endpoint.
Evidence
- Role
- authenticated
- Object
- public.get_invoice(uuid)
- Function security
- SECURITY DEFINER
- search_path
- public, pg_temp
- Executable by
- authenticated
- Target relation
- public.invoices
- Exposed via RPC
- true
- Tenant validation
- No qualifying ownership constraint detected in evaluated path
Facts above were derived by the scanner from database metadata. No model output is involved in the verdict.
Technical details
pg_proc.prosecdef is true and EXECUTE is granted to authenticated. The function body selects from public.invoices, whose tenant column is never compared against the caller's resolved tenant. Because the function owner bypasses RLS, the table's policies do not constrain the result set.
Attack path
internet → authenticated → rest → rpc → fn:public.get_invoice(uuid) → public.invoices
recommended remediation
Unlock the full security analysis
Buy full analysisConstrain the definer function to the caller's tenant, or drop definer rights and let RLS apply.
Current
CREATE OR REPLACE FUNCTION public.get_invoice(uuid) RETURNS SETOF public.invoices LANGUAGE sql SECURITY DEFINER SET search_path = public, pg_temp AS $$ SELECT * FROM public.invoices WHERE id = $1; $$;
Proposed
CREATE OR REPLACE FUNCTION public.get_invoice(uuid)
RETURNS SETOF public.invoices
LANGUAGE sql
SECURITY DEFINER
SET search_path = public, pg_temp
AS $$
SELECT *
FROM public.invoices
WHERE id = $1
AND organization_id = current_tenant_owner();
$$;
REVOKE EXECUTE ON FUNCTION public.get_invoice(uuid) FROM public;
GRANT EXECUTE ON FUNCTION public.get_invoice(uuid) TO authenticated;Expected security effect
- Authentication required
- Tenant ownership enforced inside the definer boundary
- Cross-tenant access path removed
- EXECUTE narrowed to authenticated
Compatibility risk
low
Internal jobs that call this function outside a user session will resolve no tenant and receive zero rows. Give them a separate service-role path.