CVE-2023-47643 (published 2023-11-21T20:15Z)

SuiteCRM is a Customer Relationship Management (CRM) software application. Prior to version 8.4.2, Graphql Introspection is enabled without authentication, exposing the scheme defining all object types, arguments, and functions. An attacker can obtain the GraphQL schema and understand the entire attack surface of the API, including sensitive fields such as UserHash. This issue is patched in version 8.4.2. There are no known workarounds.

CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)

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/salesagility/SuiteCRM-Core
commit_details: repo_url: https://github.com/salesagility/SuiteCRM-Core
author: Jack Anderson
author_date: 2023-11-06 16:03:39+00:00
msg: SuiteCRM 8.4.2 Release
repository: repo_url: https://github.com/salesagility/SuiteCRM-Core
repo_name: salesagility/SuiteCRM-Core
description: SuiteCRM - Open source CRM for the world
repo_language: PHP
owner: salesagility
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,7 @@
   <img width="180px" height="41px" src="https://suitecrm.com/wp-content/uploads/2017/12/logo.png" align="right" />
 </a>
 
-# SuiteCRM 8.4.1
+# SuiteCRM 8.4.2
 
 [![LICENSE](https://img.shields.io/github/license/suitecrm/suitecrm.svg)](https://github.com/salesagility/suitecrm/blob/hotfix/LICENSE.txt)
 [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/salesagility/SuiteCRM-Core/issues)

programming_language: Markdown
filename: VERSION
change_type: ModificationType.MODIFY
diff: @@ -1 +1 @@
-8.4.1
+8.4.2

programming_language: SQL
filename: core_services.yaml
change_type: ModificationType.MODIFY
diff: @@ -65,6 +65,8 @@ services:
       $adminOnlyModuleActions: '%system.admin_only_module_actions%'
       $navbarAdministrationOverrides: '%navbar.administration_override%'
       $quickActions: '%quick_actions%'
+      $graphqlShowDocs: '%graphql.graphql_show_docs%'
+
   _instanceof:
     App\Process\Service\ProcessHandlerInterface:
       tags: [ 'app.process.handler' ]
@@ -288,6 +290,10 @@ services:
     alias: App\Routes\Service\LegacyRouteHandler
     public: true
 
+  graphql.introspection_manager:
+    alias: App\Security\GraphqlIntrospectionManager
+    public: true
+
   entity_manager:
     alias: doctrine.orm.entity_manager
     public: true

programming_language: CoffeeScript
filename: api_platform.yaml
change_type: ModificationType.MODIFY
diff: @@ -2,9 +2,9 @@ api_platform:
   graphql:
     default_ide: graphql-playground
     graphiql:
-      enabled: true
-    graphql_playground:
       enabled: false
+    graphql_playground:
+      enabled: true
   mapping:
     paths: ['%kernel.project_dir%/core']
   patch_formats:

programming_language: Ruby
filename: security.php
change_type: ModificationType.MODIFY
diff: @@ -97,10 +97,16 @@ return static function (ContainerConfigurator $containerConfig) {
         ['path' => '^/$', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
         ['path' => '^/api', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
         ['path' => '^/api/graphql', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-        ['path' => '^/api/graphql/graphiql*', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
         ['path' => '^/', 'roles' => 'IS_AUTHENTICATED_FULLY']
     ];
 
+
+    $appEnv = $env['APP_ENV'] ?? 'prod';
+    $showDocs = $env['GRAPHQL_SHOW_DOCS'] ?? ($appEnv === 'dev');
+    if ($showDocs === 'false' || $showDocs === false) {
+        $baseAccessControl = array_merge([['path' => '^/docs', 'roles' => 'NO_ACCESS']], $baseAccessControl);
+    }
+
     $containerConfig->parameters()->set('auth.logout.redirect', false);
     $containerConfig->parameters()->set('auth.logout.path', 'logout');
 
@@ -244,6 +250,29 @@ return static function (ContainerConfigurator $containerConfig) {
             $samlMainFirewallConfig['saml']['user_factory'] = 'saml_user_factory';
         }
 
+        $samlAccessControl = [
+            ['path' => '^/login$', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/session-status$', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/logout$', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/saml/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/saml/metadata', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/saml/acs', 'roles' => 'ROLE_USER'],
+            ['path' => '^/saml/logout', 'roles' => 'ROLE_USER'],
+            ['path' => '^/logged-out', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/auth', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/auth/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/auth/session-status', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/auth/logout', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/$', 'roles' => 'ROLE_USER'],
+            ['path' => '^/api', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/api/graphql', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
+            ['path' => '^/', 'roles' => 'IS_AUTHENTICATED_FULLY']
+        ];
+
+        if (!$showDocs) {
+            $samlAccessControl = array_merge([['path' => '^/docs', 'roles' => 'NO_ACCESS']], $samlAccessControl);
+        }
+
         $containerConfig->extension('security', [
             'providers' => [
                 'app_user_provider' => [
@@ -293,25 +322,7 @@ return static function (ContainerConfigurator $containerConfig) {
                     ]
                 ],
             ]),
-            'access_control' => [
-                ['path' => '^/login$', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/session-status$', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/logout$', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/saml/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/saml/metadata', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/saml/acs', 'roles' => 'ROLE_USER'],
-                ['path' => '^/saml/logout', 'roles' => 'ROLE_USER'],
-                ['path' => '^/logged-out', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/auth', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/auth/login', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/auth/session-status', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/auth/logout', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/$', 'roles' => 'ROLE_USER'],
-                ['path' => '^/api', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/api/graphql', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/api/graphql/graphiql*', 'roles' => 'IS_AUTHENTICATED_ANONYMOUSLY'],
-                ['path' => '^/', 'roles' => 'IS_AUTHENTICATED_FULLY']
-            ]
+            'access_control' => $samlAccessControl
         ]);
 
 

programming_language: PHP
filename: api_platform.yaml
change_type: ModificationType.MODIFY
diff: @@ -9,4 +9,4 @@ swagger_ui:
 
 graphiql:
   path: /docs/graphql
-  controller: api_platform.graphql.action.graphiql
+  controller: api_platform.graphql.action.graphql_playground

programming_language: Markdown
filename: graphql_allow_introspection.yaml
change_type: ModificationType.ADD
diff: @@ -0,0 +1,2 @@
+parameters:
+  graphql.graphql_show_docs: '%env(default::bool:GRAPHQL_SHOW_DOCS)%'

programming_language: Batchfile
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 {
   "name": "common",
-  "version": "8.4.1",
+  "version": "8.4.2",
   "peerDependencies": {
     "@angular/common": "^12.1.0",
     "@angular/core": "^12.1.0",

programming_language: TypeScript
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 {
   "name": "core",
-  "version": "8.4.1",
+  "version": "8.4.2",
   "peerDependencies": {
     "@angular/common": "^12.1.0",
     "@angular/core": "^12.1.0",

programming_language: TypeScript
filename: Kernel.php
change_type: ModificationType.MODIFY
diff: @@ -129,4 +129,15 @@ class Kernel extends BaseKernel
 
         return [];
     }
+
+    /**
+     * Enable/disable graphql introspection
+     * @return void
+     */
+    public function configureGraphqlIntrospection(): void
+    {
+        if ($this->container->has('graphql.introspection_manager')) {
+            $this->container->get('graphql.introspection_manager')->configure();
+        }
+    }
 }

programming_language: PHP
filename: Version20231108164138.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Migrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\Migrations\AbstractMigration;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Auto-generated Migration: Please modify to your needs!
+ */
+final class Version20231108164138  extends BaseMigration implements ContainerAwareInterface
+{
+    use EnvHandlingMigrationTrait;
+
+    /**
+     * @var ContainerInterface
+     */
+    protected $container;
+
+    /**
+     * @var LoggerInterface
+     */
+    protected $upgradeLogger;
+
+    public function getDescription() : string
+    {
+        return 'Remove pdf from allowed_preview';
+    }
+
+    public function up(Schema $schema) : void
+    {
+        $systemConfigsHandler = $this->container->get('app.system-configs');
+        $systemConfigs = $systemConfigsHandler->getConfigs();
+        if (isset($systemConfigs['allowed_preview']) && in_array('pdf', $systemConfigs['allowed_preview'])) {
+            $key = array_search('pdf', $systemConfigs['allowed_preview']);
+            unset($systemConfigs['allowed_preview'][$key]);
+            $systemConfigsHandler->updateSystemConfig($systemConfigs);
+            $this->log('Removed PDF from allowed_preview inside config file.');
+            return;
+        }
+
+        $this->log('PDF was not found in allowed_preview config skipping...');
+
+    }
+
+    public function down(Schema $schema) : void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+
+    }
+}

programming_language: PHP
filename: GraphqlIntrospectionManager.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,64 @@
+<?php
+/**
+ * SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
+ * Copyright (C) 2023 SalesAgility Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License version 3 as published by the
+ * Free Software Foundation with the addition of the following permission added
+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
+ * IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
+ * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * 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
+ * "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
+ * feasible for technical reasons, the Appropriate Legal Notices must display
+ * the words "Supercharged by SuiteCRM".
+ */
+
+namespace App\Security;
+
+use GraphQL\Validator\DocumentValidator;
+use GraphQL\Validator\Rules\DisableIntrospection;
+
+class GraphqlIntrospectionManager
+{
+    /**
+     * @var bool|null
+     */
+    private $graphqlShowDocs;
+
+    /**
+     * @param bool|null $graphqlShowDocs
+     */
+    public function __construct(
+        ?bool $graphqlShowDocs
+    ) {
+        $this->graphqlShowDocs = $graphqlShowDocs;
+    }
+
+    /**
+     * Enable/disable graphql introspection
+     * @return void
+     */
+    public function configure(): void
+    {
+        $env = $_ENV ?? [];
+        $appEnv = $env['APP_ENV'] ?? 'prod';
+        $showDocs = $this->graphqlShowDocs ?? ($appEnv === 'dev');
+
+        if ($showDocs === false) {
+            DocumentValidator::addRule(new DisableIntrospection());
+        }
+    }
+
+}

programming_language: PHP
filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 {
   "name": "suitecrm",
-  "version": "8.4.1",
+  "version": "8.4.2",
   "scripts": {
     "ng": "ng",
     "start": "npm run start:shell",

programming_language: TypeScript
filename: index.php
change_type: ModificationType.MODIFY
diff: @@ -59,6 +59,7 @@ if (!empty($legacyRoute)) {
     }
 
 } else {
+    $kernel->configureGraphqlIntrospection();
     $response = $kernel->handle($request);
     $response->send();
     $kernel->terminate($request, $response);

programming_language: PHP
filename: AOP_Case_Updates.php
change_type: ModificationType.MODIFY
diff: @@ -106,7 +106,8 @@ class AOP_Case_Updates extends Basic
      */
     public function save($check_notify = false)
     {
-        $this->name = SugarCleaner::cleanHtml($this->name);
+
+        $this->name = SugarCleaner::cleanHtml($this->name ?? '');
         $this->parseDescription();
         parent::save($check_notify);
         if (file_exists('custom/modules/AOP_Case_Updates/CaseUpdatesHook.php')) {

programming_language: PHP