Skip to content

Wallet CSP blocks every issuer stellar.toml read; proxy them server-side instead of opening connect-src #971

Open

Description

@Miracle656

Two PRs this week (#763, #756) independently added "https:" to connect-src in frontend/wallet/middleware.js to get a run green. I reverted it both times (e8b6b184) — a bare https: lets a fetch reach any origin, and the wallet holds a signer secret in sessionStorage, so that directive is the exfiltration path the rest of the policy exists to close (a9a09a3b, #705; audit finding H1).

But they were both reaching for a real bug, and it is still here.

The wallet reads SEP-1 stellar.toml files straight from the browser, against domains that cannot be known in advance:

  • frontend/wallet/lib/federation.ts:47 — https://${domain}/.well-known/stellar.toml
  • frontend/wallet/lib/sep24.ts:34 — the anchor's toml, for TRANSFER_SERVER_SEP0024 and WEB_AUTH_ENDPOINT
  • frontend/wallet/lib/issuerToml.ts — issuer display metadata and logo URLs

connect-src is a closed allowlist, so every one of those is blocked by the browser in production, not only in e2e. That means "add asset by domain" (app/assets/page.tsx:244, which surfaces "Could not read stellar.toml"), the SEP-24 anchor lookup, and federation-address resolution are all broken on the deployed wallet. e2e/issuer-metadata.spec.ts intercepts https://circle.com/.well-known/stellar.toml, which is why relaxing the directive made a run go green.

The fix is a server-side proxy, not an open directive. The pattern is already in the repo: lib/issuerLogoProxy.ts fetches issuer logos through our own origin precisely so img-src/connect-src stay closed. A /api/toml?domain= route doing the same for SEP-1 would let connect-src stay 'self' plus the named endpoints.

Requirements for the proxy route, since it fetches a URL derived from untrusted input:

  • Validate domain as a hostname — no scheme, no path, no port, no userinfo, no IP literal — and build the URL server-side as https://<domain>/.well-known/stellar.toml. Never fetch a caller-supplied URL.
  • Refuse private and link-local address space after resolution, or this becomes an SSRF into the deploy environment's metadata service.
  • Cap the response body and the timeout; a toml is a few KB.
  • Cache per domain (issuerToml.ts already refreshes daily), and return a stable error shape rather than the upstream error text.
  • Never echo the upstream URL or any RPC URL into the response or the logs.

Then point federation.ts, sep24.ts and issuerToml.ts at it and leave middleware.js alone.

Acceptance: connect-src contains no bare scheme; adding an asset by domain works against a real issuer on the deployed wallet; e2e/issuer-metadata.spec.ts passes with the allowlist closed; a test asserts the route refuses a path-bearing, scheme-bearing and private-IP domain.

Activity

  1. Miracle656 commented on Oct 2, 2026

    @Miracle656
    OwnerAuthor

    Narrowed by 5c414ce8, which took the metadata half off this issue's plate: connect-src now lists the registered issuers' home domains (REGISTERED_ISSUER_ORIGINS in frontend/wallet/middleware.js, pinned by lib/__tests__/csp.test.ts). That is sound because loadRegisteredIssuerMetadata only ever resolves the homeDomain of an issuer already in ASSET_REGISTRY, so the set is closed — two domains today. e2e/issuer-metadata.spec.ts's first failure (expect "Issuer supplied name") was that read being blocked.

    What remains for this issue, all of it genuinely un-allowlistable:

    1. Logo bytes. acceptIssuerLogo (lib/issuerToml.ts:290) fetches the image URL from the issuer's toml directly before falling back to /api/issuer-logo. That URL is whatever the toml says — arbitrary — so it stays blocked and every logo now comes through the fallback. Working, but one wasted blocked request per logo, and issuer-metadata.spec.ts asserts the direct path (expect(imageRequests).toBe(1), and it stubs **/api/issuer-logo?** with a 502 specifically to force the direct read). Either the client should stop trying direct and go straight to the route, or the route should return the metadata too — see 2.
    2. SEP-24 anchors. lib/sep24.ts:34 fetches the toml of a caller-named anchorDomain for TRANSFER_SERVER_SEP0024 and WEB_AUTH_ENDPOINT. Not allowlistable, and blocked today.
    3. Federation. lib/federation.ts:47, same shape for a federation address's domain.

    So the proxy route is still the fix for 2 and 3, and folding 1 into it (an /api/issuer-toml that returns name, description and the checked logo in one response, replacing both the direct toml read and /api/issuer-logo) would let connect-src drop REGISTERED_ISSUER_ORIGINS again and leave one server-side path for all SEP-1 reads.

    The SSRF requirements in the issue body apply unchanged to 2 and 3, and matter more there than for the registry case: those domains come from user input.

    Note for whoever takes this — e2e/issuer-metadata.spec.ts will need rewriting alongside it. It mocks the browser's own fetches of https://circle.com/.well-known/stellar.toml and the image host, which is the right test for today's client-side read and the wrong one once the read moves to the server (the spec already notes "the logo route fetches on the server, out of Playwright's reach").

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions