Skip to content

fix: capture the customer's real public site URL at signup (not the internal site name) - #900

Merged
Navin-S-R merged 2 commits into
developfrom
fix/onboarding-capture-public-site-url
Aug 17, 2026
Merged

fix: capture the customer's real public site URL at signup (not the internal site name)#900
Navin-S-R merged 2 commits into
developfrom
fix/onboarding-capture-public-site-url

Conversation

@Navin-S-R

Copy link
Copy Markdown
Contributor

Problem

When a customer's site admin onboards from their own Frappe site (e.g. served at https://staging-fleet.klerk.in, internal site name staging.v15), the bench handed admin http://staging.v15 as frappe_site_url whenever host_name was unset. _public_origin() used allow_header_override=False for every caller, so it fell back to the internal site name. That wrong value then drove the post-payment redirect back to the customer (and the welcome-email link), sending them to an unreachable internal URL.

Fix — capture the real URL on the signup flow, keep the rest secure

The natural source of the customer's real public URL is the request Host header — but the code deliberately refused it to stop a spoofed Host from poisoning the recorded URL. All onboarding paths are authenticated (require_jarvis_admin), but only signup creates a new account, so trusting that admin's own request Host is self-scoped there; reconnect/replacement/lead target an existing account by email and must stay on the secure derivation.

  • _public_origin(trust_host=False)host_name still wins; else get_url(allow_header_override=trust_host). Only signup() passes trust_host=True; the four other callers keep the default False (unchanged, secure).
  • _is_real_public_host() — a real domain (dotted, not localhost, not an IP literal via ipaddress, alphabetic non-reserved TLD) is normalized to https://<host>; jarvis.local, staging.v15, IPs, and bare/dev hosts are left as-is, so dev benches are unaffected.
  • A malformed Host never raises — signup can't 500.

Security

Reviewed in depth: trust_host=True is unreachable by a guest (start_signup is @whitelist + require_jarvis_admin); a spoofed Host is ignored on the non-signup paths even under an authenticated session; signup always inserts a fresh Customer (no cross-tenant poisoning); admin re-validates ^https?://…; the verification magic-link uses admin's own origin, not this value; no SSRF sink.

Tests

110 green on test_jarvis (+ onboarding suites, 225 total). New tests drive the real unmocked get_url with a request Host to prove both the capture (staging-fleet.klerk.in → https) and the security guarantee (a spoofed Host is ignored on the default path). Each load-bearing guard mutation-verified; host_name-wins-under-trust_host=True covered. ruff clean.

Note

Bench-only. Existing Jarvis Customer.frappe_site_url rows already stored with the internal name need a one-time admin-side data correction (owner-run ops step, out of this PR).

…oarding-capture-public-site-url]

The bench handed admin the internal Frappe site name (e.g. http://staging.v15)
as frappe_site_url when host_name was unset, because _public_origin used
allow_header_override=False for every caller. That wrong value then drove the
post-payment redirect (and the welcome email link).

Capture the real public URL from the authenticated request Host on the SIGNUP
flow only, and normalize a real public domain to https:

- _public_origin(trust_host=False): host_name still wins; else
  get_url(allow_header_override=trust_host). Only signup() passes trust_host=True
  (a new account, self-scoped, require_jarvis_admin-gated). reconnect / replacement
  / lead keep the secure no-Host derivation - they target an existing account by
  email, so a spoofed Host must not choose their recorded URL.
- _is_real_public_host(): a real domain is dotted, not localhost, not an IP literal
  (ipaddress), with an alphabetic non-reserved TLD - so a real domain is https-forced
  while jarvis.local / staging.v15 / IPs / dev hosts are left as-is (dev unaffected).
- Malformed Host never raises (signup can't 500).

Bench-only; the one-time correction of existing bad rows is an admin ops step.
Hard-trim to match the file's density; no logic change.
@Navin-S-R
Navin-S-R merged commit 8a13641 into develop Aug 17, 2026
8 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

Development

Successfully merging this pull request may close these issues.

1 participant