fix(csp): allow self-hosted Supabase Realtime WebSocket in connect-src (#954)
connect-src listed the https Supabase origin plus wss://*.supabase.co, but never the wss variant of a self-hosted Supabase URL. Supabase Realtime opens wss://<host>/realtime/v1/websocket, which CSP blocked; WebKit throws synchronously on a CSP-blocked new WebSocket(), so Safari unmounted the dashboard into the error boundary (Chromium only logs). - next.config.ts: add supabaseWsUrl (NEXT_PUBLIC_SUPABASE_WS_URL, or the Supabase URL with https to wss / http to ws) to connect-src - Dockerfile: bake a __NEXT_PUBLIC_SUPABASE_WS_URL__ sentinel, since the CSP is fixed at build time and only sed-substituted at runtime - docker-entrypoint.sh: derive the wss origin from NEXT_PUBLIC_SUPABASE_URL unless overridden, substitute the sentinel - .env.docker.example: document the optional override Hosted is unaffected: the wss form of *.supabase.co was already allowlisted, so the added token is redundant there. Fixes #893 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
982fe77f72
commit
15e5dc1a01
@@ -6,3 +6,8 @@ CRON_SECRET=generate-a-random-secret
|
||||
|
||||
# Self-hosted (Docker) flag: disables application-side MFA enforcement.
|
||||
NEXT_PUBLIC_SELF_HOSTED=true
|
||||
|
||||
# Optional: WebSocket origin allowed for Supabase Realtime in the CSP.
|
||||
# Defaults to NEXT_PUBLIC_SUPABASE_URL with https:// replaced by wss://
|
||||
# (http:// by ws://). Set only if Realtime is served from another origin.
|
||||
# NEXT_PUBLIC_SUPABASE_WS_URL=wss://your-project.supabase.co
|
||||
|
||||
@@ -29,6 +29,10 @@ COPY docker/extensions.${EXTENSIONS_PRESET}.json ./extensions.config.json
|
||||
# These get replaced at runtime by docker-entrypoint.sh so the image
|
||||
# is generic and reusable across different Supabase projects.
|
||||
ENV NEXT_PUBLIC_SUPABASE_URL=__NEXT_PUBLIC_SUPABASE_URL__
|
||||
# Realtime WebSocket origin for the CSP. Must be its own sentinel: the CSP is
|
||||
# baked into the build output, so the entrypoint cannot derive wss:// from the
|
||||
# already-substituted https URL after the fact (issue #893).
|
||||
ENV NEXT_PUBLIC_SUPABASE_WS_URL=__NEXT_PUBLIC_SUPABASE_WS_URL__
|
||||
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=__NEXT_PUBLIC_SUPABASE_ANON_KEY__
|
||||
ENV NEXT_PUBLIC_APP_URL=__NEXT_PUBLIC_APP_URL__
|
||||
ENV NEXT_PUBLIC_VAPID_PUBLIC_KEY=__NEXT_PUBLIC_VAPID_PUBLIC_KEY__
|
||||
|
||||
@@ -75,7 +75,18 @@ if [ -n "$SUBST_PATHS" ]; then
|
||||
v=${v//|/\\|}
|
||||
printf %s "$v"
|
||||
}
|
||||
# Realtime WebSocket origin for the CSP (issue #893): derive from the
|
||||
# Supabase URL unless explicitly overridden. https:// becomes wss://;
|
||||
# http:// becomes ws:// for plain-HTTP local installs. Without this token
|
||||
# in connect-src, Supabase Realtime's WebSocket is CSP-blocked on
|
||||
# self-hosted installs and WebKit crashes the dashboard.
|
||||
if [ -z "${NEXT_PUBLIC_SUPABASE_WS_URL:-}" ]; then
|
||||
NEXT_PUBLIC_SUPABASE_WS_URL=$(printf %s "$NEXT_PUBLIC_SUPABASE_URL" \
|
||||
| sed -e 's|^https://|wss://|' -e 's|^http://|ws://|')
|
||||
fi
|
||||
|
||||
E_SUPABASE_URL=$(sed_esc "$NEXT_PUBLIC_SUPABASE_URL")
|
||||
E_SUPABASE_WS_URL=$(sed_esc "$NEXT_PUBLIC_SUPABASE_WS_URL")
|
||||
E_SUPABASE_ANON_KEY=$(sed_esc "$NEXT_PUBLIC_SUPABASE_ANON_KEY")
|
||||
E_APP_URL=$(sed_esc "$NEXT_PUBLIC_APP_URL")
|
||||
E_VAPID_PUBLIC_KEY=$(sed_esc "${NEXT_PUBLIC_VAPID_PUBLIC_KEY:-}")
|
||||
@@ -96,6 +107,7 @@ if [ -n "$SUBST_PATHS" ]; then
|
||||
| tr '\n' '\0' \
|
||||
| xargs -0 -r sed -i \
|
||||
-e "s|__NEXT_PUBLIC_SUPABASE_URL__|${E_SUPABASE_URL}|g" \
|
||||
-e "s|__NEXT_PUBLIC_SUPABASE_WS_URL__|${E_SUPABASE_WS_URL}|g" \
|
||||
-e "s|__NEXT_PUBLIC_SUPABASE_ANON_KEY__|${E_SUPABASE_ANON_KEY}|g" \
|
||||
-e "s|__NEXT_PUBLIC_APP_URL__|${E_APP_URL}|g" \
|
||||
-e "s|__NEXT_PUBLIC_VAPID_PUBLIC_KEY__|${E_VAPID_PUBLIC_KEY}|g" \
|
||||
|
||||
+16
-1
@@ -11,6 +11,21 @@ const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
|
||||
|
||||
// WebSocket origin for Supabase Realtime. Hosted projects are covered by the
|
||||
// wss://*.supabase.co wildcard below, but a SELF-HOSTED Supabase URL is not:
|
||||
// Realtime opens wss://<supabase-host>/realtime/v1/websocket, and WebKit
|
||||
// throws synchronously on a CSP-blocked `new WebSocket()`, unmounting the
|
||||
// dashboard into the error boundary (issue #893). The Docker image bakes the
|
||||
// __NEXT_PUBLIC_SUPABASE_WS_URL__ sentinel at build time and
|
||||
// docker-entrypoint.sh substitutes the real value at runtime (a build-time
|
||||
// https-to-wss replace would only rewrite the sentinel); the fallback derives
|
||||
// wss:/ws: from the https/http URL for non-Docker builds where the real URL
|
||||
// is present at build time. Empty supabaseUrl stays empty, mirroring how
|
||||
// ${supabaseUrl} is interpolated below (extra whitespace is valid in CSP).
|
||||
const supabaseWsUrl =
|
||||
process.env.NEXT_PUBLIC_SUPABASE_WS_URL ??
|
||||
supabaseUrl.replace(/^http(s?):/, "ws$1:");
|
||||
|
||||
const cspDirectives = [
|
||||
"default-src 'self'",
|
||||
// Recapt: scoped to the two specific hosts the SDK actually contacts:
|
||||
@@ -18,7 +33,7 @@ const cspDirectives = [
|
||||
// ingestion. The previous wildcard (`https://*.recapt.app`) allowed
|
||||
// exfiltration to any subdomain of recapt.app and is intentionally
|
||||
// narrowed.
|
||||
`connect-src 'self' ${supabaseUrl} https://*.supabase.co wss://*.supabase.co https://*.enablebanking.com https://api.recapt.app https://cdn.recapt.app`,
|
||||
`connect-src 'self' ${supabaseUrl} ${supabaseWsUrl} https://*.supabase.co wss://*.supabase.co https://*.enablebanking.com https://api.recapt.app https://cdn.recapt.app`,
|
||||
`style-src 'self' 'unsafe-inline' https://*.enablebanking.com`,
|
||||
`script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""} https://*.enablebanking.com https://cdn.recapt.app`,
|
||||
"img-src 'self' data: blob: https:",
|
||||
|
||||
Reference in New Issue
Block a user