VYPR
Critical severityNVD Advisory· Published Feb 24, 2026· Updated Feb 27, 2026

Statamic is vulnerable to account takeover via password reset link injection

CVE-2026-27593

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.

PackageAffected versionsPatched versions
statamic/cmsPackagist
< 5.73.105.73.10
statamic/cmsPackagist
>= 6.0.0-alpha.1, < 6.7.16.7.1

Affected products

2

Patches

3
6fdd03324982

wip

https://github.com/statamic/cmsJason VargaFeb 20, 2026via ghsa
2 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)

https://github.com/statamic/cmsJason VargaFeb 19, 2026via ghsa
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);
    
b2be592ddfb5

validate password reset url

https://github.com/statamic/cmsJason VargaFeb 19, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.