Statamic is vulnerable to account takeover via password reset link injection
Description
Statmatic is a Laravel and Git powered content management system (CMS). Prior to versions 6.3.3 and 5.73.10, an attacker may leverage a vulnerability in the password reset feature to capture a user's token and reset the password on their behalf. The attacker must know the email address of a valid account on the site, and the actual user must blindly click the link in their email even though they didn't request the reset. This has been fixed in 6.3.3 and 5.73.10.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Statamic CMS password reset feature allows token capture via link injection, enabling account takeover if user clicks a malicious link.
Vulnerability
Overview
CVE-2026-27593 is a vulnerability in the Statamic CMS password reset feature that allows an attacker to capture a user's password reset token and reset the password on their behalf. The root cause is insufficient validation of the password reset URL, enabling an attacker to inject a mechanism that intercepts the token when the victim clicks a link in their email [1][4].
Exploitation
To exploit this vulnerability, an attacker must know the email address of a valid account on the site. The attacker initiates a password reset request for that account, and the victim receives a legitimate-looking email containing a reset link. If the victim blindly clicks the link—even though they did not request the reset—the attacker can capture the token and use it to reset the password [1][4].
Impact
Successful exploitation allows the attacker to take over the victim's account by resetting the password. This can lead to unauthorized access to sensitive content, data manipulation, or further compromise of the site [1][4].
Mitigation
The vulnerability has been patched in Statamic CMS versions 6.7.1 and 5.73.10. Note that an initial fix in version 6.3.3 was found to be insufficient, and the correct patched version for the 6.x branch is 6.7.1 [3][4]. Users are strongly advised to upgrade to these versions or later.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
statamic/cmsPackagist | < 5.73.10 | 5.73.10 |
statamic/cmsPackagist | >= 6.0.0-alpha.1, < 6.7.1 | 6.7.1 |
Affected products
2- statamic/cmsv5Range: < 5.73.10
Patches
32 files changed · +160 −5
src/Http/Controllers/ForgotPasswordController.php+12 −5 modified@@ -10,7 +10,6 @@ use Statamic\Facades\Site; use Statamic\Facades\URL; use Statamic\Http\Middleware\RedirectIfAuthenticated; -use Statamic\Support\Str; class ForgotPasswordController extends Controller { @@ -35,10 +34,18 @@ public function sendResetLinkEmail(Request $request) if ($url = $request->_reset_url) { $url = URL::makeAbsolute($url); - $isExternal = Site::all() - ->map(fn ($site) => $site->absoluteUrl()) - ->filter(fn ($siteUrl) => Str::startsWith($url, $siteUrl)) - ->isEmpty(); + $urlDomain = parse_url($url, PHP_URL_HOST); + $currentRequestDomain = parse_url(url()->to('/'), PHP_URL_HOST); + + $isExternal = $urlDomain + ? Site::all() + ->map(fn ($site) => parse_url($site->absoluteUrl(), PHP_URL_HOST)) + ->push($currentRequestDomain) + ->filter(fn ($siteDomain) => ! is_null($siteDomain)) + ->unique() + ->filter(fn ($siteDomain) => $siteDomain === $urlDomain) + ->isEmpty() + : false; throw_if($isExternal, ValidationException::withMessages([ '_reset_url' => trans('validation.url', ['attribute' => '_reset_url']),
tests/Auth/ForgotPasswordTest.php+148 −0 added@@ -0,0 +1,148 @@ +<?php + +namespace Tests\Auth; + +use Illuminate\Support\Facades\Password; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use Statamic\Facades\User; +use Tests\PreventSavingStacheItemsToDisk; +use Tests\TestCase; + +class ForgotPasswordTest extends TestCase +{ + use PreventSavingStacheItemsToDisk; + + protected function resolveApplicationConfiguration($app) + { + parent::resolveApplicationConfiguration($app); + + $app['config']->set('app.url', 'http://absolute-url-resolved-from-request.com'); + } + + #[Test] + #[DataProvider('externalProvider')] + public function it_validates_reset_url_when_sending_reset_link_email($url, $isExternal) + { + $this->setSites([ + 'a' => ['name' => 'A', 'locale' => 'en_US', 'url' => 'http://this-site.com/'], + 'b' => ['name' => 'B', 'locale' => 'en_US', 'url' => 'http://subdomain.this-site.com/'], + 'c' => ['name' => 'C', 'locale' => 'fr_FR', 'url' => '/fr/'], + ]); + + $this->simulateSuccessfulPasswordResetEmail(); + + User::make() + ->email('san@holo.com') + ->password('chewy') + ->save(); + + $response = $this->post('/!/auth/password/email', [ + 'email' => 'san@holo.com', + '_reset_url' => $url, + ]); + + if ($isExternal) { + $response->assertSessionHasErrors(['_reset_url']); + + return; + } + + $response->assertSessionHasNoErrors(); + } + + public static function externalProvider() + { + return [ + ['http://this-site.com', false], + ['http://this-site.com?foo', false], + ['http://this-site.com#anchor', false], + ['http://this-site.com/', false], + ['http://this-site.com/?foo', false], + ['http://this-site.com/#anchor', false], + + ['http://that-site.com', true], + ['http://that-site.com/', true], + ['http://that-site.com/?foo', true], + ['http://that-site.com/#anchor', true], + ['http://that-site.com/some-slug', true], + ['http://that-site.com/some-slug?foo', true], + ['http://that-site.com/some-slug#anchor', true], + + ['http://subdomain.this-site.com', false], + ['http://subdomain.this-site.com/', false], + ['http://subdomain.this-site.com/?foo', false], + ['http://subdomain.this-site.com/#anchor', false], + ['http://subdomain.this-site.com/some-slug', false], + ['http://subdomain.this-site.com/some-slug?foo', false], + ['http://subdomain.this-site.com/some-slug#anchor', false], + + ['http://absolute-url-resolved-from-request.com', false], + ['http://absolute-url-resolved-from-request.com/', false], + ['http://absolute-url-resolved-from-request.com/?foo', false], + ['http://absolute-url-resolved-from-request.com/?anchor', false], + ['http://absolute-url-resolved-from-request.com/some-slug', false], + ['http://absolute-url-resolved-from-request.com/some-slug?foo', false], + ['http://absolute-url-resolved-from-request.com/some-slug#anchor', false], + ['/', false], + ['/?foo', false], + ['/#anchor', false], + ['/some-slug', false], + ['?foo', false], + ['#anchor', false], + ['', false], + [null, false], + + // External domain that starts with a valid domain. + ['http://this-site.com.au', true], + ['http://this-site.com.au/', true], + ['http://this-site.com.au/?foo', true], + ['http://this-site.com.au/#anchor', true], + ['http://this-site.com.au/some-slug', true], + ['http://this-site.com.au/some-slug?foo', true], + ['http://this-site.com.au/some-slug#anchor', true], + ['http://subdomain.this-site.com.au', true], + ['http://subdomain.this-site.com.au/', true], + ['http://subdomain.this-site.com.au/?foo', true], + ['http://subdomain.this-site.com.au/#anchor', true], + ['http://subdomain.this-site.com.au/some-slug', true], + ['http://subdomain.this-site.com.au/some-slug?foo', true], + ['http://subdomain.this-site.com.au/some-slug#anchor', true], + ]; + } + + #[Test] + public function it_allows_reset_url_for_current_request_domain_when_not_in_sites_config() + { + $this->setSites([ + 'a' => ['name' => 'A', 'locale' => 'en_US', 'url' => 'http://this-site.com/'], + ]); + + $this->simulateSuccessfulPasswordResetEmail(); + + User::make() + ->email('san@holo.com') + ->password('chewy') + ->save(); + + $this + ->post('/!/auth/password/email', [ + 'email' => 'san@holo.com', + '_reset_url' => 'http://absolute-url-resolved-from-request.com/some-slug', + ]) + ->assertSessionHasNoErrors(); + } + + protected function simulateSuccessfulPasswordResetEmail() + { + $success = new class + { + public function sendResetLink() + { + return Password::RESET_LINK_SENT; + } + }; + + Password::shouldReceive('broker')->andReturn($success); + } +}
78e63dfcf705[5.x] Validate password reset url (#14008)
1 file changed · +15 −1
src/Http/Controllers/ForgotPasswordController.php+15 −1 modified@@ -6,8 +6,11 @@ use Illuminate\Support\Facades\Password; use Statamic\Auth\Passwords\PasswordReset; use Statamic\Auth\SendsPasswordResetEmails; +use Statamic\Exceptions\ValidationException; +use Statamic\Facades\Site; use Statamic\Facades\URL; use Statamic\Http\Middleware\RedirectIfAuthenticated; +use Statamic\Support\Str; class ForgotPasswordController extends Controller { @@ -30,7 +33,18 @@ public function showLinkRequestForm() public function sendResetLinkEmail(Request $request) { if ($url = $request->_reset_url) { - PasswordReset::resetFormUrl(URL::makeAbsolute($url)); + $url = URL::makeAbsolute($url); + + $isExternal = Site::all() + ->map(fn ($site) => $site->absoluteUrl()) + ->filter(fn ($siteUrl) => Str::startsWith($url, $siteUrl)) + ->isEmpty(); + + throw_if($isExternal, ValidationException::withMessages([ + '_reset_url' => trans('validation.url', ['attribute' => '_reset_url']), + ])); + + PasswordReset::resetFormUrl($url); } return $this->traitSendResetLinkEmail($request);
b2be592ddfb5validate password reset url
1 file changed · +15 −1
src/Http/Controllers/ForgotPasswordController.php+15 −1 modified@@ -6,8 +6,11 @@ use Illuminate\Support\Facades\Password; use Statamic\Auth\Passwords\PasswordReset; use Statamic\Auth\SendsPasswordResetEmails; +use Statamic\Exceptions\ValidationException; +use Statamic\Facades\Site; use Statamic\Facades\URL; use Statamic\Http\Middleware\RedirectIfAuthenticated; +use Statamic\Support\Str; class ForgotPasswordController extends Controller { @@ -30,7 +33,18 @@ public function showLinkRequestForm() public function sendResetLinkEmail(Request $request) { if ($url = $request->_reset_url) { - PasswordReset::resetFormUrl(URL::makeAbsolute($url)); + $url = URL::makeAbsolute($url); + + $isExternal = Site::all() + ->map(fn ($site) => $site->absoluteUrl()) + ->filter(fn ($siteUrl) => Str::startsWith($url, $siteUrl)) + ->isEmpty(); + + throw_if($isExternal, ValidationException::withMessages([ + '_reset_url' => trans('validation.url', ['attribute' => '_reset_url']), + ])); + + PasswordReset::resetFormUrl($url); } return $this->traitSendResetLinkEmail($request);
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-jxq9-79vj-rgvwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27593ghsaADVISORY
- github.com/statamic/cms/commit/6fdd03324982848e8754f2edd2265262d361714eghsax_refsource_MISCWEB
- github.com/statamic/cms/commit/78e63dfcf705b116d5ac0f7f7f5a1a69be63d1beghsax_refsource_MISCWEB
- github.com/statamic/cms/commit/b2be592ddfb588bcb88c9be454f3590e14b145b0ghsax_refsource_MISCWEB
- github.com/statamic/cms/releases/tag/v5.73.10ghsax_refsource_MISCWEB
- github.com/statamic/cms/releases/tag/v6.3.3ghsax_refsource_MISCWEB
- github.com/statamic/cms/security/advisories/GHSA-jxq9-79vj-rgvwghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.