[TikiWiki-commits] [Git][tikiwiki/tiki][29.x] [FIX] SQL injection in installer DB-user provisioning

"Alfred Syatsukwa \(@alfredsyatsukwa\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a8654c4df53c_3818cd3810313@gitlab-sidekiq-low-urgency-cpu-bound-v2-77d778bd8d-bsshc.mail>

Alfred Syatsukwa pushed to branch 29.x at Tiki Wiki CMS Groupware / Tiki


Commits:
8413104e by Alfred Syatsukwa at 2026-08-20T01:07:40+00:00
[FIX] SQL injection in installer DB-user provisioning
---
* [FIX] SQL injection in installer DB-user provisioning
---

See merge request tikiwiki/tiki!11000

(cherry picked from commit 26d8327e52f62f124e03046ca0906a5ea3be325d)

See merge request tikiwiki/tiki!11001

- - - - -


3 changed files:

- installer/installlib.php
- installer/tiki-installer.php
- templates/tiki-install.tpl


Changes:

=====================================
installer/installlib.php
=====================================
@@ -72,6 +72,80 @@ function write_local_php($host_tiki, $user_tiki, $pass_tiki, $dbs_tiki, $client_
     }
 }
 
+/**
+ * Validate identifiers used during installer database setup.
+ *
+ * Keep the accepted character set aligned with the installer UI to avoid
+ * surprising users while still rejecting characters that can alter SQL
+ * structure or PHP configuration output.
+ */
+function installer_is_valid_mysql_identifier($value)
+{
+    return is_string($value) && preg_match('/^[A-Za-z0-9$_-]+$/', $value) === 1;
+}
+
+/**
+ * Installer-created database users should only be tied to localhost when the
+ * installation itself comes from localhost. Otherwise keep the existing
+ * wildcard behaviour for remote setup flows.
+ */
+function installer_normalize_mysql_grant_host($host)
+{
+    if (preg_match('/^(127\.0\.\d{1,3}\.\d{1,3}|localhost)(:\d+)?$/', $host)) {
+        return 'localhost';
+    }
+
+    return '%';
+}
+
+/**
+ * Build the CREATE DATABASE statement used during installation.
+ *
+ * Database names are SQL identifiers, so they must be validated separately
+ * before they are interpolated into DDL.
+ */
+function installer_build_create_database_sql($dbname)
+{
+    if (! installer_is_valid_mysql_identifier($dbname)) {
+        throw new InvalidArgumentException('Invalid database name.');
+    }
+
+    return "CREATE DATABASE IF NOT EXISTS `$dbname` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;";
+}
+
+/**
+ * Build the ALTER DATABASE statement used to normalize the default charset.
+ */
+function installer_build_alter_database_charset_sql($dbname)
+{
+    if (! installer_is_valid_mysql_identifier($dbname)) {
+        throw new InvalidArgumentException('Invalid database name.');
+    }
+
+    return "ALTER DATABASE `$dbname` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
+}
+
+/**
+ * Build the GRANT statement used when the installer creates a dedicated
+ * database user.
+ *
+ * The database name is an SQL identifier, while the user, host pattern, and
+ * password are SQL string literals that must go through qstr()/PDO::quote().
+ */
+function installer_build_grant_database_privileges_sql($dbTiki, $dbname, $user, $host, $pass)
+{
+    if (! installer_is_valid_mysql_identifier($dbname)) {
+        throw new InvalidArgumentException('Invalid database name.');
+    }
+
+    $quotedUser = $dbTiki->qstr($user);
+    $quotedHost = $dbTiki->qstr(installer_normalize_mysql_grant_host($host));
+    $quotedPass = $dbTiki->qstr($pass);
+
+    return "GRANT ALL PRIVILEGES ON `$dbname`.* TO "
+        . $quotedUser . '@' . $quotedHost . " IDENTIFIED BY " . $quotedPass . ';';
+}
+
 /**
  * @param string $domain
  * @return string
@@ -416,20 +490,19 @@ function initTikiDB($host, $user, $pass, $dbname, $client_charset, &$dbTiki)
         }
         $dbcon = ! empty($dbTiki);
         // First check that suggested database name will not cause issues
-        $dbname_clean = preg_replace('/[^a-zA-Z0-9$_-]/', "", $dbname);
-        if ($dbname_clean != $dbname) {
-            Feedback::error(tra("Some invalid characters were detected in database name. Please use alphanumeric characters (A-Z a-z 0-9) or underscore (_) or hyphen (-).", '', false, [$dbname_clean]));
+        if (! installer_is_valid_mysql_identifier($dbname)) {
+            Feedback::error(tra("Invalid database name. Use only letters, numbers, dollar signs ($), underscores (_), or hyphens (-)."));
             $dbcon = false;
         } elseif ($dbcon) {
             $error = '';
-            $sql = "CREATE DATABASE IF NOT EXISTS `$dbname_clean` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;";
+            $sql = installer_build_create_database_sql($dbname);
             $dbTiki->queryError($sql, $error);
             if (empty($error)) {
                 // assure the DB has the right default encoding (if the DB already existed)
-                $dbTiki->query("ALTER DATABASE `$dbname_clean` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
-                Feedback::success(tra("Database `%0` was created.", '', false, [$dbname_clean]));
+                $dbTiki->query(installer_build_alter_database_charset_sql($dbname));
+                Feedback::success(tra("Database `%0` was created.", '', false, [$dbname]));
             } else {
-                Feedback::error(tra("Database `%0` creation failed. You need to create the database.", '', false, [$dbname_clean]));
+                Feedback::error(tra("Database `%0` creation failed. You need to create the database.", '', false, [$dbname]));
             }
 
             try {
@@ -447,7 +520,7 @@ function initTikiDB($host, $user, $pass, $dbname, $client_charset, &$dbTiki)
                 Feedback::error($e->getMessage());
             }
         } else {
-            Feedback::error(tra("Database `%0`. Unable to connect to database.", '', false, [$dbname_clean]));
+            Feedback::error(tra("Database `%0`. Unable to connect to database.", '', false, [$dbname]));
         }
     }
 
@@ -471,14 +544,17 @@ function initTikiDB($host, $user, $pass, $dbname, $client_charset, &$dbTiki)
 function createTikiDBUser(&$dbTiki, $host, $user, $pass, $dbname)
 {
     $error = '';
-    if (preg_match('/^(127\.0\.\d{1,3}\.\d{1,3}|localhost)(:\d+)?$/', $host)) {
-        $host = 'localhost';
-    } else {
-        $host = '%';
+    if (! installer_is_valid_mysql_identifier($dbname)) {
+        Feedback::error(tra("Invalid database name. Use only letters, numbers, dollar signs ($), underscores (_), or hyphens (-)."));
+        return false;
     }
 
-    $pass = addslashes($pass);
-    $sql = "GRANT ALL PRIVILEGES ON `$dbname`.* TO `$user`@`$host` IDENTIFIED BY '$pass';";
+    if (! installer_is_valid_mysql_identifier($user)) {
+        Feedback::error(tra("Invalid database user. Use only letters, numbers, dollar signs ($), underscores (_), or hyphens (-)."));
+        return false;
+    }
+
+    $sql = installer_build_grant_database_privileges_sql($dbTiki, $dbname, $user, $host, $pass);
     $dbTiki->queryError($sql, $error);
 
     if (empty($error)) {
@@ -496,6 +572,9 @@ function createTikiDBUser(&$dbTiki, $host, $user, $pass, $dbname)
 function convert_database_to_utf8($dbname)
 {
     $db = TikiDb::get();
+    if (! installer_is_valid_mysql_identifier($dbname)) {
+        throw new InvalidArgumentException('Invalid database name.');
+    }
 
     if ($result = $db->fetchAll('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?', $dbname)) {
         $db->query("ALTER DATABASE `$dbname` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");


=====================================
installer/tiki-installer.php
=====================================
@@ -270,6 +270,7 @@ $smarty->assign('mid', 'tiki-install.tpl');
 $smarty->assign('virt', isset($virt) ? $virt : null);
 $smarty->assign('multi', isset($multi) ? $multi : null);
 $smarty->assign('lang', $language);
+$smarty->assign('allow_create_new_user', ! file_exists($local));
 if (isset($multi)) {
     $smarty->assign('default_server_domain_name', $multi);
 } elseif (isset($_SERVER['HTTP_HOST'])) {
@@ -396,9 +397,21 @@ if (
     if (! empty($_POST['user']) && strlen($_POST['user']) > 80) {
         $dbconn = false;
         Feedback::error(tra('Invalid database user.'));
+    } elseif (empty($_POST['user'])) {
+        $dbconn = false;
+        Feedback::error(tra('No database user specified'));
     } elseif (empty($_POST['name'])) {
         $dbconn = false;
         Feedback::error(tra('No database name specified'));
+    } elseif (! installer_is_valid_mysql_identifier($_POST['name'])) {
+        $dbconn = false;
+        Feedback::error(tra("Invalid database name. Use only letters, numbers, dollar signs ($), underscores (_), or hyphens (-)."));
+    } elseif (! installer_is_valid_mysql_identifier($_POST['user'])) {
+        $dbconn = false;
+        Feedback::error(tra("Invalid database user. Use only letters, numbers, dollar signs ($), underscores (_), or hyphens (-)."));
+    } elseif (! empty($_POST['create_new_user']) && file_exists($local)) {
+        $dbconn = false;
+        Feedback::error(tra('Creating a new database user is only available during initial installation.'));
     } else {
         if (isset($_POST['force_utf8'])) {
             $client_charset = 'utf8mb4';


=====================================
templates/tiki-install.tpl
=====================================
@@ -338,7 +338,7 @@
                                     <div class="mb-3 row">
                                         <label class="col-form-label" for="name">{tr}Database name:{/tr}</label>
                                         <div class="mx-3">
-                                            <input type="text" class="form-control" id="name" name="name" size="40" value="{if isset($smarty.request.name)}{$smarty.request.name|escape:"html"}{elseif isset($preconfigname)}{$preconfigname|escape:"html"}{/if}" placeholder="{tr}Database name{/tr}"/>
+                                            <input type="text" class="form-control" id="name" name="name" size="40" value="{if isset($smarty.request.name)}{$smarty.request.name|escape:"html"}{elseif isset($preconfigname)}{$preconfigname|escape:"html"}{/if}" placeholder="{tr}Database name{/tr}" pattern="[A-Za-z0-9_$-]+" title="{tr}Use only letters, numbers, dollar signs, underscores, or hyphens.{/tr}"/>
                                             <a href="javascript:void(0)" onclick="flip('name_help');" title="{tr}Help{/tr}">
                                                 {icon name="help"}
                                             </a>
@@ -356,7 +356,7 @@
                                     <legend>{tr}Database user{/tr}</legend>
                                     <p>{tr}Enter a database user with administrator permission for the Tiki database.{/tr}</p>
                                     <div style="padding:5px;">
-                                        <label class="col-form-label" for="user">{tr}User name:{/tr}</label> <input type="text" class="form-control" id="user" name="user" value="{if (isset($smarty.request.user))}{$smarty.request.user|escape:"html"}{elseif isset($preconfiguser)}{$preconfiguser|escape:"html"}{/if}" maxlength="80" placeholder="{tr}Database username{/tr}">
+                                        <label class="col-form-label" for="user">{tr}User name:{/tr}</label> <input type="text" class="form-control" id="user" name="user" value="{if (isset($smarty.request.user))}{$smarty.request.user|escape:"html"}{elseif isset($preconfiguser)}{$preconfiguser|escape:"html"}{/if}" maxlength="80" placeholder="{tr}Database username{/tr}" pattern="[A-Za-z0-9_$-]+" title="{tr}Use only letters, numbers, dollar signs, underscores, or hyphens.{/tr}">
                                     </div>
 
                                     <div style="padding:5px;">
@@ -367,42 +367,49 @@
                                         {/if}
                                     </div>
 
-                                    <div style="padding:5px;">
-                                        <input type="checkbox" id="create-new-user" name="create_new_user" />
-                                        <label class="col-form-label" for="create-new-user">{tr}Create the above database user just for this Tiki database.{/tr}</label>&nbsp;
-                                    </div>
-                                </fieldset>
-
-                                <br/>
-                                <fieldset id="new-user-fieldset" style="display: none;">
-                                    <legend>{tr}Administrative database user{/tr}</legend>
-                                    <p>{tr}Enter database administrator user name and password.{/tr}<br>
-                                        <em>{tr}This is a DB admin user which has permission to create new databases and new users.{/tr}</em></p>
-                                    <div style="padding:5px;">
-                                        <label class="col-form-label" for="user">{tr}DB admin user name:{/tr}</label> <input type="text" class="form-control" id="root_user" name="root_user" value="{if (isset($smarty.request.root_user))}{$smarty.request.root_user|escape:"html"}{elseif isset($preconfiguser)}{$preconfiguser|escape:"html"}{/if}" placeholder="{tr}DB admin user name{/tr}">
-                                    </div>
-                                    <div style="padding:5px;">
-                                        <label class="col-form-label" for="pass">{tr}DB admin password:{/tr}</label> <input type="password" class="form-control" id="root_pass" name="root_pass" value="{if (isset($smarty.request.root_pass))}{$smarty.request.root_pass|escape:"html"}{/if}" autocomplete="new-password">
-                                    </div>
+                                    {if $allow_create_new_user}
+                                        <div style="padding:5px;">
+                                            <input type="checkbox" id="create-new-user" name="create_new_user" />
+                                            <label class="col-form-label" for="create-new-user">{tr}Create the above database user just for this Tiki database.{/tr}</label>&nbsp;
+                                        </div>
+                                    {else}
+                                        <div style="padding:5px;">
+                                            <em>{tr}Creating a new database user is only available during the initial installation.{/tr}</em>
+                                        </div>
+                                    {/if}
                                 </fieldset>
-                                <script type='text/javascript'><!--//--><![CDATA[//><!--
-                                    ;(function(){
-                                        var user = document.getElementById('user');
-                                        var create_new_user = document.getElementById('create-new-user');
-                                        var new_user_fs = document.getElementById('new-user-fieldset');
 
-                                        if(create_new_user.checked) {
-                                            new_user_fs.style.display = 'block';
-                                        }
+                                {if $allow_create_new_user}
+                                    <br/>
+                                    <fieldset id="new-user-fieldset" style="display: none;">
+                                        <legend>{tr}Administrative database user{/tr}</legend>
+                                        <p>{tr}Enter database administrator user name and password.{/tr}<br>
+                                            <em>{tr}This is a DB admin user which has permission to create new databases and new users.{/tr}</em></p>
+                                        <div style="padding:5px;">
+                                            <label class="col-form-label" for="user">{tr}DB admin user name:{/tr}</label> <input type="text" class="form-control" id="root_user" name="root_user" value="{if (isset($smarty.request.root_user))}{$smarty.request.root_user|escape:"html"}{elseif isset($preconfiguser)}{$preconfiguser|escape:"html"}{/if}" placeholder="{tr}DB admin user name{/tr}">
+                                        </div>
+                                        <div style="padding:5px;">
+                                            <label class="col-form-label" for="pass">{tr}DB admin password:{/tr}</label> <input type="password" class="form-control" id="root_pass" name="root_pass" value="{if (isset($smarty.request.root_pass))}{$smarty.request.root_pass|escape:"html"}{/if}" autocomplete="new-password">
+                                        </div>
+                                    </fieldset>
+                                    <script type='text/javascript'><!--//--><![CDATA[//><!--
+                                        ;(function(){
+                                            var create_new_user = document.getElementById('create-new-user');
+                                            var new_user_fs = document.getElementById('new-user-fieldset');
 
-                                        create_new_user.addEventListener('click', function(){
-                                            if(create_new_user.checked) {
+                                            if (create_new_user.checked) {
                                                 new_user_fs.style.display = 'block';
-                                            } else {
-                                                new_user_fs.style.display = 'none';
                                             }
-                                        });
-                                    })();//--><!]]></script>
+
+                                            create_new_user.addEventListener('click', function(){
+                                                if (create_new_user.checked) {
+                                                    new_user_fs.style.display = 'block';
+                                                } else {
+                                                    new_user_fs.style.display = 'none';
+                                                }
+                                            });
+                                        })();//--><!]]></script>
+                                {/if}
 
                                 <br/>
                                 <input type="hidden" name="resetdb" value="y">



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/8413104e5c595560ffd29f5eb01edfb290ebb784

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/8413104e5c595560ffd29f5eb01edfb290ebb784
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.