[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX] SQL injection in installer DB-user provisioning
"Alfred Syatsukwa \(@alfredsyatsukwa\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <6a85d9802b08f_3818c9285482e@gitlab-sidekiq-low-urgency-cpu-bound-v2-7b9bd87b46-ht8nh.mail> |
Alfred Syatsukwa pushed to branch master at Tiki Wiki CMS Groupware / Tiki Commits: 85843a24 by Alfred Syatsukwa at 2026-08-19T16:08:43+00:00 [FIX] SQL injection in installer DB-user provisioning --- * [FIX] Extract installer SQL builders for DB-user provisioning Keep database names as validated identifiers and quote user/host/password as SQL literals. Adds tests for unsafe database names and GRANT host normalization. (cherry picked from commit 8099e0feca28f53b385bca143d61e0ad85237723) Co-authored-by: Cursor <[email protected]> * [FIX] SQL injection in installer DB-user provisioning (cherry picked from commit d1926c29add85d9604c0191ff1c32d3404d8c345) See merge request tikiwiki/tiki!10973 - - - - - 4 changed files: - doc/devtools/run_local_checks.php - installer/installlib.php - installer/tiki-installer.php - templates/tiki-install.tpl Changes: ===================================== doc/devtools/run_local_checks.php ===================================== @@ -70,6 +70,12 @@ function listFiles(array $files): string { return implode(' ', $files); } + +function quoteFiles(array $files): string +{ + return implode(' ', array_map('escapeshellarg', $files)); +} + function hasComposerChanges(array $files): bool { foreach ($files as $file) { @@ -104,7 +110,16 @@ if (! empty($phpFiles)) { ]; $steps[] = [ 'PHPLint', - 'php vendor_bundled/vendor/overtrue/phplint/bin/phplint ' . listFiles($phpFiles) . ' --no-interaction --no-cache --progress path', + 'php vendor_bundled/vendor/overtrue/phplint/bin/phplint ' . quoteFiles($phpFiles) . ' --no-interaction --no-cache --progress path', + ]; + $steps[] = [ + 'Rector', + 'php bin/rector process --dry-run ' . quoteFiles($phpFiles), + ]; + + $steps[] = [ + 'PHPStan', + 'php bin/phpstan --memory-limit=' . escapeshellarg($phpstanMemoryLimit) . ' --configuration=phpstan-tikiCi.neon analyse ' . quoteFiles($phpFiles), ]; } ===================================== 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 ===================================== @@ -273,6 +273,7 @@ $smarty->assign('mid', 'tiki-install.tpl'); $smarty->assign('virt', $virt ?? null); $smarty->assign('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'])) { @@ -399,9 +400,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> - </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> + </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/85843a2480420f0189d2f130234d28ad4248435e -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/85843a2480420f0189d2f130234d28ad4248435e 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