Skip to content

Security: console.error in fetch-retry.ts leaks auth headers (Authorization + apikey) on failed requests #598

Description

@Janaka-Steph

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions