CVE-2024-24818 (PHP, published 2024-03-21T02:52Z)
Description: value: EspoCRM is an Open Source Customer Relationship Management software. An attacker can inject arbitrary IP or domain in "Password Change" page and redirect victim to malicious page that could lead to  credential stealing or another attack. This vulnerability is fixed in 8.1.2.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/espocrm/espocrm
Files changed:
application/Espo/Tools/UserSecurity/Password/Recovery/UrlValidator.php
application/Espo/Tools/UserSecurity/Password/RecoveryService.php
tests/integration/Espo/Password/RecoveryTest.php
Commit message: validate url


Vulnerable code:
        private AuthenticationMethodProvider $authenticationMethodProvider

Fixed code:
<?php
/************************************************************************
 * This file is part of EspoCRM.
 *
 * EspoCRM – Open Source CRM application.
 * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
 * Website: https://www.espocrm.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
 ************************************************************************/

namespace Espo\Tools\UserSecurity\Password\Recovery;

use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Utils\Config;
use Espo\Entities\Portal;
use Espo\ORM\EntityManager;

class UrlValidator
{
    public function __construct(
        private Config $config,
        private EntityManager $entityManager
    ) {}

    /**
     * @throws Forbidden
     */
    public function validate(string $url): void
    {
        $siteUrl = rtrim($this->config->get('siteUrl') ?? '', '/');

        if (str_starts_with($url, $siteUrl)) {
            return;
        }

        /** @var iterable<Portal> $portals */
        $portals = $this->entityManager
            ->getRDBRepositoryByClass(Portal::class)
            ->find();

        foreach ($portals as $portal) {
            $siteUrl = rtrim($portal->getUrl() ?? '', '/');

            if (str_starts_with($url, $siteUrl)) {
                return;
            }
        }

        throw new Forbidden("URL does not match Site URL.");
    }
}
use Espo\Tools\UserSecurity\Password\Recovery\UrlValidator;
        private AuthenticationMethodProvider $authenticationMethodProvider,
        private UrlValidator $urlValidator
        if ($url) {
            $this->urlValidator->validate($url);
        }

<?php
/************************************************************************
 * This file is part of EspoCRM.
 *
 * EspoCRM – Open Source CRM application.
 * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
 * Website: https://www.espocrm.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
 * these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
 ************************************************************************/

namespace tests\integration\Espo\Password;

use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Utils\Config\ConfigWriter;
use Espo\Entities\Portal;
use Espo\Tools\UserSecurity\Password\Recovery\UrlValidator;
use tests\integration\Core\BaseTestCase;

class RecoveryTest extends BaseTestCase
{
    private ?string $storedSiteUrl = null;

    private string $siteUrl = 'https://my-site.com/';

    protected function setUp(): void
    {
        parent::setUp();

        $writer = $this->getInjectableFactory()->create(ConfigWriter::class);
        $writer->set('siteUrl', $this->siteUrl);
        $writer->save();

        $this->storedSiteUrl = $this->getConfig()->get('siteUrl');
    }

    protected function tearDown(): void
    {
        $writer = $this->getInjectableFactory()->create(ConfigWriter::class);
        $writer->set('siteUrl', $this->storedSiteUrl);
        $writer->save();

        $this->storedSiteUrl = null;

        parent::tearDown();
    }

    public function testUrlValidation()
    {
        $em = $this->getEntityManager();

        $em->createEntity(Portal::ENTITY_TYPE, [
            'customUrl' => 'https://my-portal.com/',
        ]);

        /** @var Portal $portal2 */
        $portal2 = $em->createEntity(Portal::ENTITY_TYPE, [
            'isDefault' => true,
        ]);

        $validator = $this->getInjectableFactory()->create(UrlValidator::class);

        /** @noinspection PhpUnhandledExceptionInspection */
        $validator->validate('https://my-site.com');

        /** @noinspection PhpUnhandledExceptionInspection */
        $validator->validate('https://my-site.com/');

        /** @noinspection PhpUnhandledExceptionInspection */
        $validator->validate('https://my-site.com#Test');

        /** @noinspection PhpUnhandledExceptionInspection */
        $validator->validate('https://my-site.com/portal');

        /** @noinspection PhpUnhandledExceptionInspection */
        $validator->validate('https://my-portal.com');

        /** @noinspection PhpUnhandledExceptionInspection */
        $validator->validate('https://my-portal.com/');

        /** @noinspection PhpUnhandledExceptionInspection */
        $validator->validate('https://my-portal.com/#Test');

        /** @noinspection PhpUnhandledExceptionInspection */
        $validator->validate('https://my-site.com/portal/' . $portal2->getId());

        $thrown = false;

        try {
            $validator->validate('https://not-my-site.com');
        }
        catch (Forbidden) {
            $thrown = true;
        }

        $this->assertTrue($thrown);
    }
}