CVE-2024-24818 (published 2024-03-21T02:52Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/espocrm/espocrm
commit_details: repo_url: https://github.com/espocrm/espocrm
author: Yuri Kuznetsov
author_date: 2024-01-30 17:40:38+02:00
msg: validate url
repository: repo_url: https://github.com/espocrm/espocrm
repo_name: espocrm/espocrm
description: EspoCRM – Open Source CRM Application
repo_language: PHP
owner: espocrm
file_changes: filename: UrlValidator.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,70 @@
+<?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.");
+    }
+}

programming_language: PHP
filename: RecoveryService.php
change_type: ModificationType.MODIFY
diff: @@ -56,6 +56,7 @@ use Espo\Core\Utils\Config;
 use Espo\Core\Utils\Log;
 use Espo\Core\Utils\TemplateFileManager;
 use Espo\Tools\UserSecurity\Password\Jobs\RemoveRecoveryRequest;
+use Espo\Tools\UserSecurity\Password\Recovery\UrlValidator;
 
 class RecoveryService
 {
@@ -75,7 +76,8 @@ class RecoveryService
         private Log $log,
         private JobSchedulerFactory $jobSchedulerFactory,
         private ApplicationState $applicationState,
-        private AuthenticationMethodProvider $authenticationMethodProvider
+        private AuthenticationMethodProvider $authenticationMethodProvider,
+        private UrlValidator $urlValidator
     ) {}
 
     /**
@@ -140,6 +142,10 @@ class RecoveryService
             throw new Forbidden("Password recovery: Disabled.");
         }
 
+        if ($url) {
+            $this->urlValidator->validate($url);
+        }
+
         /** @var ?User $user */
         $user = $this->entityManager
             ->getRDBRepository(User::ENTITY_TYPE)

programming_language: PHP
filename: RecoveryTest.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,116 @@
+<?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);
+    }
+}

programming_language: PHP