CVE-2023-46726 (published 2023-12-13T19:15Z)

GLPI is a free asset and IT management software package. Starting in version 10.0.0 and prior to version 10.0.11, on PHP 7.4 only, the LDAP server configuration form can be used to execute arbitrary code previously uploaded as a GLPI document. Version 10.0.11 contains a patch for the issue.

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

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/glpi-project/glpi
commit_details: repo_url: https://github.com/glpi-project/glpi
author: Cédric Anne
author_date: 2023-12-13 08:19:00+01:00
msg: Merge pull request from GHSA-qc92-gxc6-5f95
repository: repo_url: https://github.com/glpi-project/glpi
repo_name: glpi-project/glpi
description: GLPI is a Free Asset and IT Management Software package, Data center management, ITIL Service Desk, licenses tracking and software auditing.
repo_language: PHP
owner: glpi-project
file_changes: filename: AuthLDAP.php
change_type: ModificationType.MODIFY
diff: @@ -35,6 +35,7 @@
 
 use Glpi\Application\ErrorHandler;
 use Glpi\Application\View\TemplateRenderer;
+use Glpi\Toolbox\Filesystem;
 use Glpi\Toolbox\Sanitizer;
 
 /**
@@ -306,7 +307,10 @@ class AuthLDAP extends CommonDBTM
             };
         }
 
-        $this->checkFilesExist($input);
+        if (!$this->checkFilesExist($input)) {
+            return false;
+        }
+
         return $input;
     }
 
@@ -3119,19 +3123,24 @@ class AuthLDAP extends CommonDBTM
                 );
             }
         }
-        if (
-            !empty($tls_certfile)
-            && file_exists($tls_certfile)
-            && !@ldap_set_option(null, LDAP_OPT_X_TLS_CERTFILE, $tls_certfile)
-        ) {
-            trigger_error("Unable to set LDAP option `LDAP_OPT_X_TLS_CERTFILE`", E_USER_WARNING);
+
+        if (!empty($tls_certfile)) {
+            if (!Filesystem::isFilepathSafe($tls_certfile)) {
+                trigger_error("TLS certificate path is not safe.", E_USER_WARNING);
+            } elseif (!file_exists($tls_certfile)) {
+                trigger_error("TLS certificate path is not valid.", E_USER_WARNING);
+            } elseif (!@ldap_set_option(null, LDAP_OPT_X_TLS_CERTFILE, $tls_certfile)) {
+                trigger_error("Unable to set LDAP option `LDAP_OPT_X_TLS_CERTFILE`", E_USER_WARNING);
+            }
         }
-        if (
-            !empty($tls_keyfile)
-            && file_exists($tls_keyfile)
-            && !@ldap_set_option(null, LDAP_OPT_X_TLS_KEYFILE, $tls_keyfile)
-        ) {
-            trigger_error("Unable to set LDAP option `LDAP_OPT_X_TLS_KEYFILE`", E_USER_WARNING);
+        if (!empty($tls_keyfile)) {
+            if (!Filesystem::isFilepathSafe($tls_keyfile)) {
+                trigger_error("TLS key file path is not safe.", E_USER_WARNING);
+            } elseif (!file_exists($tls_keyfile)) {
+                trigger_error("TLS key file path is not valid.", E_USER_WARNING);
+            } elseif (!@ldap_set_option(null, LDAP_OPT_X_TLS_KEYFILE, $tls_keyfile)) {
+                trigger_error("Unable to set LDAP option `LDAP_OPT_X_TLS_KEYFILE`", E_USER_WARNING);
+            }
         }
 
         if ($use_tls) {
@@ -4759,30 +4768,31 @@ class AuthLDAP extends CommonDBTM
 
     public function checkFilesExist(&$input)
     {
-
-        if (isset($input['tls_certfile'])) {
-            $file = realpath($input['tls_certfile']);
-            if (!file_exists($file)) {
-                Session::addMessageAfterRedirect(
-                    __('TLS certificate path is incorrect'),
-                    false,
-                    ERROR
-                );
-                return false;
-            }
+        if (
+            isset($input['tls_certfile'])
+            && (!Filesystem::isFilepathSafe($input['tls_certfile']) || !file_exists($input['tls_certfile']))
+        ) {
+            Session::addMessageAfterRedirect(
+                __('TLS certificate path is incorrect'),
+                false,
+                ERROR
+            );
+            return false;
         }
 
-        if (isset($input['tls_keyfile'])) {
-            $file = realpath($input['tls_keyfile']);
-            if (!file_exists($file)) {
-                Session::addMessageAfterRedirect(
-                    __('TLS key file path is incorrect'),
-                    false,
-                    ERROR
-                );
-                return false;
-            }
+        if (
+            isset($input['tls_keyfile'])
+            && (!Filesystem::isFilepathSafe($input['tls_keyfile']) || !file_exists($input['tls_keyfile']))
+        ) {
+            Session::addMessageAfterRedirect(
+                __('TLS key file path is incorrect'),
+                false,
+                ERROR
+            );
+            return false;
         }
+
+        return true;
     }
 
 

programming_language: PHP
filename: Filesystem.php
change_type: ModificationType.MODIFY
diff: @@ -35,7 +35,7 @@
 
 namespace Glpi\Toolbox;
 
-class Filesystem
+final class Filesystem
 {
     /**
      * Checks if the file with given path can be written.
@@ -80,4 +80,64 @@ class Filesystem
 
         return true;
     }
+
+    /**
+     * Checks if the given file path is safe.
+     *
+     * @param string $path
+     * @param string $restricted_directory
+     *
+     * @return bool
+     */
+    public static function isFilepathSafe(string $path, ?string $restricted_directory = null): bool
+    {
+        $parsed_scheme = parse_url($path, PHP_URL_SCHEME);
+
+        if ($parsed_scheme === 'file') {
+            // If scheme is `file://`, parse the path again to validate that it does not contains itself
+            // an unexpected scheme.
+            $path = parse_url($path, PHP_URL_PATH);
+            $parsed_scheme = parse_url($path, PHP_URL_SCHEME);
+        }
+
+        if ($parsed_scheme !== null && preg_match('/^[a-z]$/i', $parsed_scheme) !== 1) {
+            // As soon as the path contains a scheme, it is not considered as safe,
+            // unless the scheme is 1 letter (corresponds to a drive letter on Windows system).
+            return false;
+        }
+
+        if ($restricted_directory === null) {
+            // All directories are allowed.
+            return true;
+        }
+
+        $restricted_directory = self::normalizePath($restricted_directory);
+        if (!str_ends_with($restricted_directory, '/')) {
+            // Ensure directory ends with a `/`, to prevent false positives:
+            // - /path/to/dir/file is inside /path/to/dir
+            // - /path/to/dir_file is not inside /path/to/dir
+            $restricted_directory .= '/';
+        }
+
+        return str_starts_with(self::normalizePath($path), $restricted_directory);
+    }
+
+    /**
+     * Normalize a path, to make comparisons and relative paths computation easier.
+     *
+     * @param string $path
+     * @return string
+     */
+    private static function normalizePath(string $path): string
+    {
+        $realpath = realpath($path);
+        if ($realpath !== false) {
+            // Use realpath if possible (not always possible, for instance when file not exists).
+            $path = $realpath;
+        }
+
+        // Normalize all directory separators to `/`.
+        $path = preg_replace('/\\\/', '/', $path);
+        return $path;
+    }
 }

programming_language: PHP
filename: Filesystem.php
change_type: ModificationType.MODIFY
diff: @@ -39,7 +39,7 @@ use org\bovigo\vfs\vfsStream;
 
 class Filesystem extends \GLPITestCase
 {
-    public function testCanWriteFile()
+    public function testCanWriteFile(): void
     {
         $config_dir = vfsStream::setup('config');
 
@@ -70,4 +70,114 @@ class Filesystem extends \GLPITestCase
         $this->boolean($this->testedInstance->canWriteFile(vfsStream::url('config/whatever.yml')))->isEqualTo(true);
         $this->boolean($this->testedInstance->canWriteFiles([vfsStream::url('config/config_db.php'), vfsStream::url('config/whatever.yml')]))->isEqualTo(true);
     }
+
+    protected function isFilepathSafeProvider(): iterable
+    {
+        // Unix paths and file scheme
+        foreach (['', 'file://'] as $prefix) {
+            yield [
+                'path'                  => $prefix . '/path/to/whatever/file',
+                'restricted_directory'  => null,
+                'is_safe'               => true,
+            ];
+            yield [
+                'path'                  => $prefix . '/path/to/whatever/file',
+                'restricted_directory'  => '/path/to/whatever',
+                'is_safe'               => true,
+            ];
+            yield [
+                'path'                  => $prefix . '/path/to/whatever/file/in/a/subdir',
+                'restricted_directory'  => '/path/to/whatever',
+                'is_safe'               => true,
+            ];
+            yield [
+                'path'                  => $prefix . '/path/to/whatever_file',
+                'restricted_directory'  => '/path/to/whatever',
+                'is_safe'               => false,
+            ];
+            yield [
+                'path'                  => $prefix . '/path/to/whatever/file',
+                'restricted_directory'  => '/safedir',
+                'is_safe'               => false,
+            ];
+        }
+
+        // Windows paths (`\` separator)
+        yield [
+            'path'                  => 'C:\\path\\to\\whatever\\file',
+            'restricted_directory'  => null,
+            'is_safe'               => true,
+        ];
+        yield [
+            'path'                  => 'C:\\path\\to\\whatever\\file',
+            'restricted_directory'  => 'C:\\path\\to\\whatever',
+            'is_safe'               => true,
+        ];
+        yield [
+            'path'                  => 'C:\\path\\to\\whatever\\file\\in\\a\\subdir',
+            'restricted_directory'  => 'C:\\path\\to\\whatever',
+            'is_safe'               => true,
+        ];
+        yield [
+            'path'                  => 'C:\\path\\to\\whatever_file',
+            'restricted_directory'  => 'C:\\path\\to\\whatever',
+            'is_safe'               => false,
+        ];
+        yield [
+            'path'                  => 'C:\\path\\to\\whatever\\file',
+            'restricted_directory'  => 'C:\\safedir',
+            'is_safe'               => false,
+        ];
+
+        // Windows path on file scheme
+        yield [
+            'path'                  => 'file:///C:/path/to/whatever/file',
+            'restricted_directory'  => null,
+            'is_safe'               => true,
+        ];
+        yield [
+            'path'                  => 'file:///C:/path/to/whatever/file',
+            'restricted_directory'  => 'C:\\path\\to\\whatever',
+            'is_safe'               => true,
+        ];
+        yield [
+            'path'                  => 'file:///C:/path/to/whatever/file/in/a/subdir',
+            'restricted_directory'  => 'C:\\path\\to\\whatever',
+            'is_safe'               => true,
+        ];
+        yield [
+            'path'                  => 'file:///C:/path/to/whatever_file',
+            'restricted_directory'  => 'C:\\path\\to\\whatever',
+            'is_safe'               => false,
+        ];
+        yield [
+            'path'                  => 'file:///C:/path/to/whatever/file',
+            'restricted_directory'  => 'C:\\safedir',
+            'is_safe'               => false,
+        ];
+
+        // Streams and remote paths
+        foreach (['ftp', 'http', 'https', 'phar', 'whateverstream'] as $scheme) {
+            yield [
+                'path'                  => $scheme . '://path/to/whatever/file',
+                'restricted_directory'  => null,
+                'is_safe'               => false, // path using scheme is never considered to be safe
+            ];
+
+            yield [
+                'path'                  => $scheme . '://path/to/whatever/file',
+                'restricted_directory'  => $scheme . '://path/to/whatever',
+                'is_safe'               => false, // path using scheme is never considered to be safe
+            ];
+        }
+    }
+
+    /**
+     * @dataProvider isFilepathSafeProvider
+     */
+    public function testIsFilepathSafe(string $path, ?string $restricted_directory, bool $is_safe): void
+    {
+        $this->newTestedInstance();
+        $this->boolean($this->testedInstance->isFilepathSafe($path, $restricted_directory))->isEqualTo($is_safe);
+    }
 }

programming_language: PHP