From 27132d1175ad931d591d40286b1c9864483ca13b Mon Sep 17 00:00:00 2001 From: Peter Knut Date: Fri, 19 Jan 2024 00:29:25 +0100 Subject: [PATCH] Validate server input - Allow only scheme, host and port in the server field. - Use proper default host and port in Elasticsearch and ClickHouse driver. --- adminer/drivers/elastic.inc.php | 13 ++++--- adminer/include/auth.inc.php | 62 +++++++++++++++++++++++++++++---- plugins/drivers/clickhouse.php | 10 ++++-- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/adminer/drivers/elastic.inc.php b/adminer/drivers/elastic.inc.php index f03098ec..a0cc4c18 100644 --- a/adminer/drivers/elastic.inc.php +++ b/adminer/drivers/elastic.inc.php @@ -58,9 +58,15 @@ if (isset($_GET["elastic"])) { return $this->rootQuery(($this->_db != "" ? "$this->_db/" : "/") . ltrim($path, '/'), $content, $method); } + /** + * @param string $server + * @param string $username + * @param string $password + * @return bool + */ function connect($server, $username, $password) { - preg_match('~^(https?://)?(.*)~', $server, $match); - $this->_url = ($match[1] ? $match[1] : "http://") . "$username:$password@$match[2]"; + $this->_url = build_http_url($server, $username, $password, "localhost", 9200); + $return = $this->query(''); if ($return) { $this->server_info = $return['version']['number']; @@ -227,9 +233,6 @@ if (isset($_GET["elastic"])) { global $adminer; $connection = new Min_DB; list($server, $username, $password) = $adminer->credentials(); - if (strpos($server, '/') !== false || strpos($server, ':') !== false) { - return lang('Only hostname or IP address'); - } if ($password != "" && $connection->connect($server, $username, "")) { return lang('Database does not support password.'); } diff --git a/adminer/include/auth.inc.php b/adminer/include/auth.inc.php index 8669351f..b26a543b 100644 --- a/adminer/include/auth.inc.php +++ b/adminer/include/auth.inc.php @@ -15,6 +15,58 @@ if ($_COOKIE["adminer_permanent"]) { } } +function validate_server_input() { + if (SERVER == "") { + return; + } + + $parts = parse_url(SERVER); + if (!$parts) { + auth_error(lang('Invalid credentials.')); + } + + // Check proper URL parts. + if (isset($parts['user']) || isset($parts['pass']) || isset($parts['query']) || isset($parts['fragment'])) { + auth_error(lang('Invalid credentials.')); + } + + // Allow only HTTP/S scheme. + if (isset($parts['scheme']) && !preg_match('~^(https?)$~i', $parts['scheme'])) { + auth_error(lang('Invalid credentials.')); + } + + // Allow only host without a path. Note that "localhost" is parsed as path. + $host = (isset($parts['host']) ? $parts['host'] : '') . (isset($parts['path']) ? $parts['path'] : ''); + if (strpos(rtrim($host, '/'), '/') !== false) { + auth_error(lang('Invalid credentials.')); + } + + // Check privileged ports. + if (isset($parts['port']) && ($parts['port'] < 1024 || $parts['port'] > 65535)) { + auth_error(lang('Connecting to privileged ports is not allowed.')); + } +} + +/** + * @param string $server + * @param string $username + * @param string $password + * @param string $defaultServer + * @param int|null $defaultPort + * @return string + */ +function build_http_url($server, $username, $password, $defaultServer, $defaultPort = null) { + if (!preg_match('~^(https?://)?([^:]*)(:\d+)?$~', rtrim($server, '/'), $matches)) { + $this->error = lang('Invalid credentials.'); + return false; + } + + return ($matches[1] ?: "http://") . + ($username !== "" || $password !== "" ? "$username:$password@" : "") . + ($matches[2] !== "" ? $matches[2] : $defaultServer) . + (isset($matches[3]) ? $matches[3] : ($defaultPort ? ":$defaultPort" : "")); +} + function add_invalid_login() { global $adminer; $fp = file_open_lock(get_temp_dir() . "/adminer.invalid"); @@ -55,7 +107,7 @@ $auth = $_POST["auth"]; if ($auth) { session_regenerate_id(); // defense against session fixation $vendor = $auth["driver"]; - $server = $auth["server"]; + $server = trim($auth["server"]); $username = $auth["username"]; $password = (string) $auth["password"]; $db = $auth["db"]; @@ -81,7 +133,7 @@ if ($auth) { set_session($key, null); } unset_permanent(); - redirect(substr(preg_replace('~\b(username|db|ns)=[^&]*&~', '', ME), 0, -1), lang('Logout successful.') . '.'); + redirect(substr(preg_replace('~\b(username|db|ns)=[^&]*&~', '', ME), 0, -1), lang('Logout successful.')); } elseif ($permanent && !$_SESSION["pwds"]) { session_regenerate_id(); @@ -158,11 +210,9 @@ if (isset($_GET["username"]) && !class_exists("Min_DB")) { stop_session(true); if (isset($_GET["username"]) && is_string(get_password())) { - list($host, $port) = explode(":", SERVER, 2); - if (preg_match('~^\s*([-+]?\d+)~', $port, $match) && ($match[1] < 1024 || $match[1] > 65535)) { // is_numeric('80#') would still connect to port 80 - auth_error(lang('Connecting to privileged ports is not allowed.')); - } + validate_server_input(); check_invalid_login(); + $connection = connect(); $driver = new Min_Driver($connection); } diff --git a/plugins/drivers/clickhouse.php b/plugins/drivers/clickhouse.php index 60bb0b45..e489038f 100644 --- a/plugins/drivers/clickhouse.php +++ b/plugins/drivers/clickhouse.php @@ -55,9 +55,15 @@ if (isset($_GET["clickhouse"])) { return $this->rootQuery($this->_db, $query); } + /** + * @param string $server + * @param string $username + * @param string $password + * @return bool + */ function connect($server, $username, $password) { - preg_match('~^(https?://)?(.*)~', $server, $match); - $this->_url = ($match[1] ? $match[1] : "http://") . "$username:$password@$match[2]"; + $this->_url = build_http_url($server, $username, $password, "localhost", 8123); + $return = $this->query('SELECT 1'); return (bool) $return; }