Skip to content

Harden auth redirects, server-supplied URLs, production config and PII scrubbing (#862-#865) - #1034

Merged
DevMuhdishaq merged 2 commits into
Dfunder:mainfrom
Maryermarh:feature/862-865-auth-url-security
Oct 20, 2026
Merged

DevMuhdishaq merged 2 commits into
Dfunder:mainfrom
Maryermarh:feature/862-865-auth-url-security

Conversation

@Maryermarh

Copy link
Copy Markdown
Contributor

Summary

Four security/robustness issues on the auth, URL-handling and config surfaces, addressed together because they share the same helpers.

#862 — unvalidated ?redirect= and a bypassable safeRedirect

SignInPage navigated to a raw ?redirect=/state.from, and safeRedirect only rejected values that did not start with / or started with // — so /\evil.com and /%09/evil.com got through. On top of that, RequireAuth wrote state={{ from }} that LoginPage never read, so post-login redirects were broken app-wide.

  • safeRedirect and a new resolvePostLoginRedirect now live in src/lib/url.ts with one tested definition. They reject anything that is not a single-leading-slash app path, reject / followed by \, decode before checking (so encoded whitespace/slash variants are caught), and require new URL(value, location.origin).origin === location.origin.
  • The helper takes unknown, so router state is narrowed, not asserted. SignInPage reads notice only when it is a string.
  • One contract everywhere: state.from → ?redirect= → /dashboard, validated at the single point of use.

#863 — server-supplied URLs rendered without validation

  • New safeExternalUrl(raw, allowedHosts?) parses with new URL, accepts only https: (plus http: in dev), optionally restricts the host, and returns null otherwise.
  • A javascript:/data: deliverable URL now renders a non-clickable "link unavailable" span instead of an <a href>; AccountMenu validates user.avatarUrl before <img src>.
  • Every interpolated path parameter is encodeURIComponent-encoded (explorer links, order/payment result, transaction status, wallet balances, asset selector).
  • Seller profile links use the real sellerUsername instead of a slugified display name.

#864 — production silently defaulted to localhost and testnet

  • Production has no defaults. A missing VITE_API_URL, VITE_STELLAR_NETWORK or VITE_APP_URL in a production build fails closed; the dev defaults apply only outside a prod build.
  • A production http:// VITE_API_URL is rejected — session tokens would otherwise travel in plaintext.
  • Config resolution no longer throws at module scope. It returns loadResult (env, configError, invalidKeys) and main.tsx branches on it, so an invalid build renders ConfigErrorScreen (now listing the offending variable names) instead of a blank page.
  • Added an explicit allow-list of Stellar network passphrases (SUPPORTED_NETWORK_PASSPHRASES / isSupportedNetworkPassphrase).

#865 — PII scrubbing gaps and a token in the console

  • stripPii matches Stellar addresses case-insensitively and covers muxed M… addresses, extends the PII key list, recurses through nested objects/arrays so an email under an unrecognised key is stripped, and drops token-shaped values (JWT / opaque blobs) so no token reaches a provider.
  • reportUnexpectedError redacts token-shaped URL segments before logging — the password-reset token travels in the path.
  • Toast behaviour is opt-out per request (skipToast) and applied to background polls and probes, so a retry loop no longer spams the user.
  • RegisterPage no longer reveals that an email is already registered: a 409 is made indistinguishable from a successful sign-up, matching LoginPage's existing anti-enumeration copy.

Tests

  • New src/lib/url.test.ts: safeRedirect (including the three bypasses from the issue), resolvePostLoginRedirect, safeExternalUrl (javascript:, data:, protocol-relative, off-host), redactUrl.
  • Updated src/lib/analytics.test.ts: the tests that previously documented the lowercase-address and nested-PII gaps now assert they are stripped; added muxed-address, nested-email and token-payload cases.

Note on scope

Some files in this area carried pre-existing syntax corruption on main (duplicate imports/identifiers in useUiStore, TransactionStatusPolling, WalletBalanceDisplay, a missing useQuery import in OrderConfirmationScreen, a duplicated AuthProvider import in main.tsx). Those files are touched by this PR, so the corruption was repaired as part of applying the fix. Corrupt files outside this scope were left untouched.

Closes #862
Closes #863
Closes #864
Closes #865

Closes Dfunder#862, Dfunder#863, Dfunder#864, Dfunder#865.

Dfunder#862 - validate every post-login redirect through one shared helper
- add `safeRedirect`/`resolvePostLoginRedirect` to `src/lib/url.ts`: rejects
  `//evil.com`, `/\evil.com`, `/%09/…`, `/%2F%2F…`, scheme URLs and malformed
  escapes, and additionally requires the resolved `URL` origin to match the
  app's own origin.
- `SignInPage` no longer navigates to a raw `?redirect=`/`state.from`; it narrows
  the attacker-influenceable router state field by field instead of asserting it,
  and only renders a `notice` when it is a string.
- `LoginPage`/`GuestRoute`/`SignInPage` now share one contract: `state.from`,
  then `?redirect=`, then the dashboard default, validated at the single point
  of use. This fixes the app-wide broken post-login redirect (`RequireAuth` set
  `state.from` that `LoginPage` never read).
- tests: `src/lib/url.test.ts` plus the existing auth utils suite.

Dfunder#863 - validate server-supplied URLs before rendering them as links/sources
- add `safeExternalUrl(raw, allowedHosts?)`: parses with `new URL`, accepts only
  `https:` (plus `http:` in dev) and an optional host allow-list, else `null`.
- `PaymentResultScreen`/`OrderConfirmationScreen` deliverable links render a
  non-clickable fallback when the API returns a `javascript:`/`data:` URL.
- `AccountMenu` validates `user.avatarUrl` before it reaches `<img src>`.
- `encodeURIComponent` every interpolated path parameter (explorer links, order/
  payment result, transaction status, wallet balances, asset selector) and the
  seller profile route now uses the real `sellerUsername` instead of a
  slugified display name.
- tests: `safeExternalUrl` covers `javascript:`, `data:`, protocol-relative,
  off-host and non-string inputs.

Dfunder#864 - fail closed in production when the config is missing or wrong
- remove the production localhost/testnet defaults; required variables have no
  production fallback and dev defaults apply only outside a prod build.
- a production build rejects an `http://` `VITE_API_URL` (tokens would travel in
  plaintext).
- the config no longer throws at module scope: it exports `loadResult`
  (`env`/`configError`/`invalidKeys`) and `main.tsx` branches on it, so an
  invalid build renders `ConfigErrorScreen` instead of a blank page.
- `ConfigErrorScreen` lists the offending variable names.
- add an explicit allow-list of Stellar network passphrases
  (`SUPPORTED_NETWORK_PASSPHRASES`/`isSupportedNetworkPassphrase`).
- fix the `.env.example` wording that claimed all variables are required in
  production; docs updated in README/CONTRIBUTING.

Dfunder#865 - close the PII-filtering gaps and stop logging sensitive URLs
- `stripPii` now: matches Stellar addresses case-insensitively and covers muxed
  `M…` addresses, extends the PII key-name list, walks nested objects/arrays so
  an email under an unrecognised key is stripped, and drops token-shaped values
  (JWT/opaque) so no token reaches a provider.
- `reportUnexpectedError` redacts token-shaped URL segments before logging, so a
  password-reset token that travels in the path never reaches the console.
- toast behaviour is opt-out per request (`skipToast`) and set on background
  polls/probes, so retries no longer spam the user.
- `RegisterPage` no longer reveals that an email is already registered; a 409 is
  made indistinguishable from a successful sign-up, matching `LoginPage`'s
  existing anti-enumeration wording.
- update the analytics tests that pinned the old gaps; add nested-PII and
  token-payload cases.
@drips-wave

drips-wave Bot commented Oct 20, 2026

Copy link
Copy Markdown

@Maryermarh Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Maryermarh

Copy link
Copy Markdown
Contributor Author

@ayshadogo conflict resolved

@DevMuhdishaq
DevMuhdishaq merged commit 8c5a65b into Dfunder:main Oct 20, 2026
0 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants