From bb46f2b3d0318db4d23b561017674c83de3f274f Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 11:43:56 +0530 Subject: [PATCH 1/3] fix(site): reject site names that are not valid hostnames Validate the site name with site-command's validate_site_name() right after it is lowercased, before anything touches the shell, the file system or docker. Needs the site-command release that adds validate_site_name(). --- src/PHP.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PHP.php b/src/PHP.php index 61b7e1c..2526ac4 100644 --- a/src/PHP.php +++ b/src/PHP.php @@ -191,6 +191,7 @@ public function create( $args, $assoc_args ) { $this->logger->debug( 'args:', $args ); $this->logger->debug( 'assoc_args:', empty( $assoc_args ) ? array( 'NULL' ) : $assoc_args ); $this->site_data['site_url'] = strtolower( \EE\Utils\remove_trailing_slash( $args[0] ) ); + \EE\Site\Utils\validate_site_name( $this->site_data['site_url'] ); if ( Site::find( $this->site_data['site_url'] ) ) { \EE::error( sprintf( "Site %1\$s already exists. If you want to re-create it please delete the older one using:\n`ee site delete %1\$s`", $this->site_data['site_url'] ) ); From de6fff20df54ad00d2c2a0f19cb3dde278056f92 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 11:55:02 +0530 Subject: [PATCH 2/3] fix(site): quote remote database check commands The remote DB checks on php site create put the DB host, port, user, password and the DB name (derived from the site name by default) in hand-written quotes nested inside a double-quoted sh -c/bash -c script, so a value containing a quote or shell syntax was not passed through as one argument. Quote each value for the inner shell and the whole script for the host shell with escapeshellarg(). --- src/PHP.php | 65 ++++++++++++++--------------------------------------- 1 file changed, 17 insertions(+), 48 deletions(-) diff --git a/src/PHP.php b/src/PHP.php index 2526ac4..1f4e9d3 100644 --- a/src/PHP.php +++ b/src/PHP.php @@ -705,14 +705,19 @@ private function maybe_verify_remote_db_connection() { \EE::log( 'Verifying connection to remote database' ); - $check_db_connection = sprintf( - "docker exec %s sh -c \"mysql --host='%s' --port='%s' --user='%s' --password='%s' --execute='EXIT'\"", - $container_name, - $db_host, - $this->site_data['db_port'], - $this->site_data['db_user'], - $this->site_data['db_password'] + // Each value is quoted for the shell inside the container, then the whole script for the host shell. + $db_args = sprintf( + '--host=%s --port=%s --user=%s --password=%s', + escapeshellarg( $db_host ), + escapeshellarg( $this->site_data['db_port'] ), + escapeshellarg( $this->site_data['db_user'] ), + escapeshellarg( $this->site_data['db_password'] ) ); + $db_exec = function ( $shell, $script ) use ( $container_name ) { + return sprintf( 'docker exec %s %s -c %s', $container_name, $shell, escapeshellarg( $script ) ); + }; + + $check_db_connection = $db_exec( 'sh', "mysql $db_args --execute='EXIT'" ); if ( ! \EE::exec( $check_db_connection ) ) { \EE::exec( "docker rm -f $container_name" ); throw new \Exception( 'Unable to connect to remote db' ); @@ -720,19 +725,11 @@ private function maybe_verify_remote_db_connection() { \EE::success( 'Connection to remote db verified' ); $name = str_replace( '_', '\_', $this->site_data['db_name'] ); - $check_db_exists = sprintf( "docker exec %s bash -c \"mysqlshow --user='%s' --password='%s' --host='%s' --port='%s' '%s'\"", $container_name, $this->site_data['db_user'], $this->site_data['db_password'], $db_host, $this->site_data['db_port'], $name ); + $check_db_exists = $db_exec( 'bash', "mysqlshow $db_args " . escapeshellarg( $name ) ); if ( ! \EE::exec( $check_db_exists ) ) { \EE::log( sprintf( 'Database `%s` does not exist. Attempting to create it.', $this->site_data['db_name'] ) ); - $create_db_command = sprintf( - "docker exec %s bash -c \"mysql --host='%s' --port='%s' --user='%s' --password='%s' --execute='CREATE DATABASE %s;'\"", - $container_name, - $db_host, - $this->site_data['db_port'], - $this->site_data['db_user'], - $this->site_data['db_password'], - $this->site_data['db_name'] - ); + $create_db_command = $db_exec( 'bash', "mysql $db_args --execute=" . escapeshellarg( 'CREATE DATABASE ' . $this->site_data['db_name'] . ';' ) ); if ( ! \EE::exec( $create_db_command ) ) { \EE::exec( "docker rm -f $container_name" ); @@ -746,38 +743,10 @@ private function maybe_verify_remote_db_connection() { } } else { if ( $this->force ) { - \EE::exec( - sprintf( - "docker exec %s bash -c \"mysql --host='%s' --port='%s' --user='%s' --password='%s' --execute='DROP DATABASE %s;'\"", - $container_name, - $db_host, - $this->site_data['db_port'], - $this->site_data['db_user'], - $this->site_data['db_password'], - $this->site_data['db_name'] - ) - ); - \EE::exec( - sprintf( - "docker exec %s bash -c \"mysql --host='%s' --port='%s' --user='%s' --password='%s' --execute='CREATE DATABASE %s;'\"", - $container_name, - $db_host, - $this->site_data['db_port'], - $this->site_data['db_user'], - $this->site_data['db_password'], - $this->site_data['db_name'] - ) - ); + \EE::exec( $db_exec( 'bash', "mysql $db_args --execute=" . escapeshellarg( 'DROP DATABASE ' . $this->site_data['db_name'] . ';' ) ) ); + \EE::exec( $db_exec( 'bash', "mysql $db_args --execute=" . escapeshellarg( 'CREATE DATABASE ' . $this->site_data['db_name'] . ';' ) ) ); } - $check_tables = sprintf( - "docker exec %s bash -c \"mysql --host='%s' --port='%s' --user='%s' --password='%s' --execute='USE %s; show tables;'\"", - $container_name, - $db_host, - $this->site_data['db_port'], - $this->site_data['db_user'], - $this->site_data['db_password'], - $this->site_data['db_name'] - ); + $check_tables = $db_exec( 'bash', "mysql $db_args --execute=" . escapeshellarg( 'USE ' . $this->site_data['db_name'] . '; show tables;' ) ); $launch = \EE::launch( $check_tables ); if ( ! $launch->return_code ) { From 0de09666c7eaad7b7a685b1c21b1707ec6ab42b1 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 12:03:44 +0530 Subject: [PATCH 3/3] fix(site): validate the site name only when site-command provides the check site-command's create router validates the name as well; the guard keeps site-type-php working next to an older site-command instead of failing with an undefined function. --- src/PHP.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PHP.php b/src/PHP.php index 1f4e9d3..6d7e368 100644 --- a/src/PHP.php +++ b/src/PHP.php @@ -191,7 +191,10 @@ public function create( $args, $assoc_args ) { $this->logger->debug( 'args:', $args ); $this->logger->debug( 'assoc_args:', empty( $assoc_args ) ? array( 'NULL' ) : $assoc_args ); $this->site_data['site_url'] = strtolower( \EE\Utils\remove_trailing_slash( $args[0] ) ); - \EE\Site\Utils\validate_site_name( $this->site_data['site_url'] ); + // site-command's create router validates too; this guard keeps an older site-command working. + if ( function_exists( '\EE\Site\Utils\validate_site_name' ) ) { + \EE\Site\Utils\validate_site_name( $this->site_data['site_url'] ); + } if ( Site::find( $this->site_data['site_url'] ) ) { \EE::error( sprintf( "Site %1\$s already exists. If you want to re-create it please delete the older one using:\n`ee site delete %1\$s`", $this->site_data['site_url'] ) );