Version
@nuxtjs/supabase 2.0.5 (logging line present since PR #382, August 2024)
Summary
In src/runtime/utils/fetch-retry.ts:
if (attempt === retries) {
console.error(`Error fetching request ${req}`, error, init)
throw error
}
The third argument init is the full RequestInit passed by @supabase/ssr / @supabase/supabase-js, which includes init.headers with:
Any runtime that captures console.error output (Cloudflare Workers Observability, Vercel log drains, Sentry with the default console integration, Logpush, stdout shipping to ELK, etc.) will persist the secret key in indexed, queryable storage.
Evidence
Observed on a production Cloudflare Workers deployment using serverSupabaseServiceRole(). After a transient Too many subrequests by single Worker invocation error, the captured log entry in Cloudflare Observability contained:
Error fetching request https://<project>.supabase.co/auth/v1/admin/users/<uuid>
Error: Too many subrequests by single Worker invocation. ...
{"method":"GET","headers":{"Authorization":"Bearer sb_secret_<REDACTED>","apikey":"sb_secret_<REDACTED>","X-Client-Info":"supabase-js-node/2.103.0","X-Supabase-Api-Version":"2024-01-01"}}
The sb_secret_* value is the service_role key in plain text — visible to anyone with read access to the observability dashboard, and potentially archived via log shipping downstream.
Impact
service_role keys bypass RLS and have full database admin rights. When PR #534 wired fetchWithRetry into serverSupabaseServiceRole, this logging statement started receiving requests authenticated with the highest-privilege credential on every transient failure (network blips, rate limits, cold-start timeouts on edge runtimes).
Apps affected are those that (a) use serverSupabaseServiceRole(), (b) deploy to a runtime where console.error output is captured and persisted (Cloudflare Workers Observability, Vercel with log drains, any stdout-shipping setup), and (c) experience any fetch failure on a service_role call.
Proposed fix
Strip sensitive headers before logging, e.g.:
const SENSITIVE_HEADERS = new Set(['authorization', 'apikey', 'cookie', 'x-supabase-auth'])
function redactHeaders(headers: HeadersInit | undefined): HeadersInit | undefined {
if (!headers) return headers
const entries = headers instanceof Headers
? Array.from(headers.entries())
: Array.isArray(headers)
? headers
: Object.entries(headers)
return Object.fromEntries(
entries.map(([k, v]) => [k, SENSITIVE_HEADERS.has(k.toLowerCase()) ? '[REDACTED]' : v])
)
}
if (attempt === retries) {
const safeInit = init ? { ...init, headers: redactHeaders(init.headers) } : undefined
console.error(`Error fetching request ${req}`, error, safeInit)
throw error
}
Alternatively — log only the non-sensitive subset (method, url, status, error message). The full init payload is rarely useful for diagnosing a fetch failure: the error already carries the status and cause.
Happy to open a PR if the approach sounds right.
Severity
I'd rate this High: it's a silent leak of the highest-privilege credential, triggered automatically by any transient fetch failure from server-side code, into destinations the developer may not treat as secret. No user action required to trigger.
Version
@nuxtjs/supabase2.0.5 (logging line present since PR #382, August 2024)Summary
In
src/runtime/utils/fetch-retry.ts:The third argument
initis the fullRequestInitpassed by@supabase/ssr/@supabase/supabase-js, which includesinit.headerswith:Authorization: Bearer <token>— the service_role secret key when the call originates fromserverSupabaseServiceRole()(wired tofetchWithRetryin PR Improve error handling and add fetch retry backoff #534), or the user JWT otherwiseapikey: <token>— the same tokenAny runtime that captures
console.erroroutput (Cloudflare Workers Observability, Vercel log drains, Sentry with the default console integration, Logpush, stdout shipping to ELK, etc.) will persist the secret key in indexed, queryable storage.Evidence
Observed on a production Cloudflare Workers deployment using
serverSupabaseServiceRole(). After a transientToo many subrequests by single Worker invocationerror, the captured log entry in Cloudflare Observability contained:The
sb_secret_*value is the service_role key in plain text — visible to anyone with read access to the observability dashboard, and potentially archived via log shipping downstream.Impact
service_role keys bypass RLS and have full database admin rights. When PR #534 wired
fetchWithRetryintoserverSupabaseServiceRole, this logging statement started receiving requests authenticated with the highest-privilege credential on every transient failure (network blips, rate limits, cold-start timeouts on edge runtimes).Apps affected are those that (a) use
serverSupabaseServiceRole(), (b) deploy to a runtime whereconsole.erroroutput is captured and persisted (Cloudflare Workers Observability, Vercel with log drains, any stdout-shipping setup), and (c) experience any fetch failure on a service_role call.Proposed fix
Strip sensitive headers before logging, e.g.:
Alternatively — log only the non-sensitive subset (method, url, status, error message). The full
initpayload is rarely useful for diagnosing a fetch failure: theerroralready carries the status and cause.Happy to open a PR if the approach sounds right.
Severity
I'd rate this High: it's a silent leak of the highest-privilege credential, triggered automatically by any transient fetch failure from server-side code, into destinations the developer may not treat as secret. No user action required to trigger.