Header ownership across layers: this document covers the frontend
connect-srcdirective only. For the full picture of which layer sets which security header (backend Helmet, frontend Next CSP, wallet extensions, edge/preview), seedocs/SECURITY_HEADERS_MATRIX.md.
The Content-Security-Policy header is built in next.config.ts via
buildContentSecurityPolicy() (src/lib/csp.ts). This document covers the
connect-src directive specifically, since it's the one that controls which
hosts the app (and connected wallets like Freighter) are allowed to talk to.
connect-src is built from buildConnectSrcHosts() and always includes:
-
The app's own API origin — the host portion of
getApiBaseUrl()(NEXT_PUBLIC_API_URL, defaulting tohttp://localhost:3001). -
Default Stellar/Soroban hosts (
DEFAULT_STELLAR_CONNECT_HOSTSinsrc/lib/csp.ts):*.stellar.orgmainnet.sorobanrpc.comrpc-futurenet.stellar.orgsoroban-testnet.stellar.orghorizon-testnet.stellar.orghorizon-futurenet.stellar.org
-
Whatever RPC hosts are actually configured, extracted from:
NEXT_PUBLIC_SOROBAN_RPC_URLNEXT_PUBLIC_HORIZON_URL
This matters when the app is pointed at a non-default RPC — a private Soroban RPC endpoint, a different testnet provider, a self-hosted Horizon instance, etc. Without step 3, CSP would silently block those calls even though
contract-config.tsis configured to use them, and wallet signing/submission would fail with an opaque CSP violation in the console instead of a clear error. -
In local dev only:
localhost:*and127.0.0.1:*, so local backends and local RPC nodes work without any extra configuration. This is gated byshouldAllowLocalhost()— it is dropped for a production build and for any deployed environment that identifies itself viaNEXT_PUBLIC_APP_ENV/VERCEL_ENVaspreview,staging, orproduction, even if that build was made withNODE_ENV !== 'production'. A preview URL therefore never ships a policy that trustslocalhost.
Everything else is blocked. A host that isn't the API origin, isn't in
DEFAULT_STELLAR_CONNECT_HOSTS, and isn't reachable via
NEXT_PUBLIC_SOROBAN_RPC_URL / NEXT_PUBLIC_HORIZON_URL will not be
added automatically — this is intentional, so a compromised or unexpected
host can't sneak into the policy.
Browser wallet extensions (Freighter, Lobstr, Rabet, …) inject their bridge
into the page and communicate with the extension's own origin. That traffic
is not subject to the page's connect-src — extension messaging goes
through the injected provider / window.postMessage, not fetch/WebSocket
from the page — so no extension origin needs to be listed in connect-src.
The extension's own background context performs the network calls to Horizon
and Soroban RPC, and those are governed by the extension's own permissions,
not this app's CSP.
What the app does need for Freighter to work is:
- The Horizon / Soroban RPC hosts the extension will be asked to talk to must be reachable from the page when the app itself reads chain state (covered by items 1–3 above).
- The cross-origin isolation headers must be relaxed on wallet-heavy routes (see COEP / CORP and wallet extensions below).
Do not add extension origins (e.g. chrome-extension://…) to
connect-src. They are unnecessary and would widen the policy without
benefit. If a wallet integration genuinely needs a page-initiated network
call to a wallet vendor host, add that host explicitly via the env-driven
mechanism below rather than a wildcard.
WalletConnect (Sign Client) is off by default and gated behind a feature
flag. When it is disabled, the app makes no WalletConnect calls and the
connect-src policy does not need any WalletConnect relay hosts — the
policy above is complete and nothing extra is added.
When WalletConnect is enabled (flag on and a project id configured),
the Sign Client opens a WebSocket to the WalletConnect relay and the browser
will block it unless the relay host is in connect-src. The relay hosts are
added to connect-src only in that enabled case, so a default/off build
never widens the policy:
wss://relay.walletconnect.comwss://relay.walletconnect.orghttps://verify.walletconnect.com(attestation / verify API)
These are added by buildConnectSrcHosts() in src/lib/csp.ts when the
WalletConnect feature flag is enabled and a project id is present. If the
flag is on but the project id is missing, the app surfaces a clear
configuration error instead of silently attempting a relay connection that
CSP would block anyway.
Enabling WalletConnect therefore requires a CSP change in the same
deploy — flip the flag, set the project id, and confirm the relay hosts
above appear in the served Content-Security-Policy header. See
docs/WALLET_SETUP.md for the full enable steps and
STAGING_PARITY_CHECKLIST.md for the staging verification checklist.
Production connect-src must never contain a bare wildcard (*) or a
scheme-only wildcard (https:, wss:). Every entry is either an explicit
host, a scoped subdomain wildcard that is part of the reviewed default set
(*.stellar.org), or a host derived from an explicit env var. This keeps
XSS containment meaningful: a compromised script can only exfiltrate to the
small, reviewed set of hosts the app legitimately talks to.
If you find yourself wanting connect-src * to "make something work", the
correct fix is to add the specific host via NEXT_PUBLIC_SOROBAN_RPC_URL /
NEXT_PUBLIC_HORIZON_URL (or to DEFAULT_STELLAR_CONNECT_HOSTS if it's a
new Stellar default), not to widen the policy.
A report-uri / report-to directive can be added to the CSP to collect
violation reports. It is optional and off unless a reporting endpoint is
configured; when set, it must point at a first-party collector. Reporting is
observability only — it does not relax any directive, and a missing or
unreachable report endpoint must never cause the policy itself to fail open.
- Adding a new default Stellar/Soroban host (e.g. Stellar ships a new
network or RPC provider): add it to
DEFAULT_STELLAR_CONNECT_HOSTSinsrc/lib/csp.ts. - Using a custom/private RPC or Horizon endpoint: set
NEXT_PUBLIC_SOROBAN_RPC_URLand/orNEXT_PUBLIC_HORIZON_URLin your env (see.env.example) — no code change needed, the host is picked up automatically. - Enabling WalletConnect: set the feature flag and project id (see
docs/WALLET_SETUP.md); the relay hosts are added toconnect-srcautomatically when the flag is on. Do not add relay hosts manually for a flag-off build. - Verifying nothing regressed: run the CSP regression test in
src/lib/csp.test.ts(npm test -- csp). It asserts the default Stellar hosts are always present and that a configured RPC/Horizon host is added, so an accidental deletion of a host fromDEFAULT_STELLAR_CONNECT_HOSTSor a change tobuildConnectSrcHosts()that drops env-configured hosts fails CI instead of shipping a broken wallet connection.
src/lib/csp.test.ts is a CI regression test for connect-src. It exists
because CSP is easy to break silently — a refactor of next.config.ts or
src/lib/csp.ts can drop a Stellar host and nothing fails locally; it only
shows up later as a wallet extension throwing CSP violations in production.
What it asserts:
- Every host in
DEFAULT_STELLAR_CONNECT_HOSTS(*.stellar.org,mainnet.sorobanrpc.com,rpc-futurenet.stellar.org,soroban-testnet.stellar.org,horizon-testnet.stellar.org,horizon-futurenet.stellar.org) is present inconnect-src. - The API origin host is present.
- A host configured via
NEXT_PUBLIC_SOROBAN_RPC_URL/NEXT_PUBLIC_HORIZON_URLis added and deduped against the defaults. - Hosts that aren't the API origin, a default Stellar host, or an env-configured RPC/Horizon host are not added.
localhost/127.0.0.1are present in local dev and absent in preview/staging/production.
docs/SECURITY_HEADERS_MATRIX.md— single source of truth for which layer (backend Helmet, frontend Next CSP, wallet extensions, edge/preview) sets which security header, with per-environment examples and verification steps.docs/WALLET_SETUP.md— wallet enable steps, including the WalletConnect flag/project-id flow referenced above.backend/docs/CORS_AND_SECURITY_HEADERS.md— backend Helmet/CORS header ownership and configuration.