From 183d37afea72ca31e143020dd54c984d8ac14b7d Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 10:34:34 +0530 Subject: [PATCH 1/4] 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/WordPress.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WordPress.php b/src/WordPress.php index e4516d1..aab868b 100644 --- a/src/WordPress.php +++ b/src/WordPress.php @@ -278,6 +278,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'] ); $mu = \EE\Utils\get_flag_value( $assoc_args, 'mu' ); $this->site_data['app_sub_type'] = $mu ?? 'wp'; From 19812bdaeba7a7246fa9a715a7f8e77c79149308 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 10:45:49 +0530 Subject: [PATCH 2/4] fix(site): quote site-derived values in wp create commands The page cache key prefixes were interpolated unquoted, and the wp core install URL, the wp config create DB name and user, and the remote DB checks used hand-written quotes (nested inside a double-quoted bash -c script for the remote DB checks), so a value containing a quote or shell syntax was not passed through as one argument. Quote each value with escapeshellarg(), nesting it for the sh -c/bash -c scripts. --- src/WordPress.php | 79 ++++++++++++++--------------------------------- 1 file changed, 24 insertions(+), 55 deletions(-) diff --git a/src/WordPress.php b/src/WordPress.php index aab868b..00123ca 100644 --- a/src/WordPress.php +++ b/src/WordPress.php @@ -509,10 +509,10 @@ private function enable_page_cache() { \EE_DOCKER::docker_compose_exec( "wp config set RT_WP_NGINX_HELPER_REDIS_HOSTNAME $redis_host --add=true --type=constant", 'php', 'bash', 'www-data' ); \EE_DOCKER::docker_compose_exec( "wp config set RT_WP_NGINX_HELPER_REDIS_PORT 6379 --add=true --type=constant", 'php', 'bash', 'www-data' ); - \EE_DOCKER::docker_compose_exec( "wp config set RT_WP_NGINX_HELPER_REDIS_PREFIX $page_cache_key_prefix --add=true --type=constant", 'php', 'bash', 'www-data' ); - \EE_DOCKER::docker_compose_exec( "wp config set WP_CACHE_KEY_SALT $obj_cache_key_prefix --add=true --type=constant", 'php', 'bash', 'www-data' ); + \EE_DOCKER::docker_compose_exec( 'wp config set RT_WP_NGINX_HELPER_REDIS_PREFIX ' . escapeshellarg( $page_cache_key_prefix ) . ' --add=true --type=constant', 'php', 'bash', 'www-data' ); + \EE_DOCKER::docker_compose_exec( 'wp config set WP_CACHE_KEY_SALT ' . escapeshellarg( $obj_cache_key_prefix ) . ' --add=true --type=constant', 'php', 'bash', 'www-data' ); \EE_DOCKER::docker_compose_exec( "wp config set WP_REDIS_MAXTTL 14400 --add=true --type=constant", 'php', 'bash', 'www-data' ); - \EE_DOCKER::docker_compose_exec( "wp $wp_cli_params rt_wp_nginx_helper_options '$plugin_data' --format=json", 'php', 'bash', 'www-data' ); + \EE_DOCKER::docker_compose_exec( "wp $wp_cli_params rt_wp_nginx_helper_options " . escapeshellarg( $plugin_data ) . ' --format=json', 'php', 'bash', 'www-data' ); } @@ -899,14 +899,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' ); @@ -914,19 +919,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" ); @@ -940,38 +937,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 ) { @@ -1209,7 +1178,7 @@ private function wp_download_and_config( $assoc_args ) { } $db_host = isset( $this->site_data['db_port'] ) ? $this->site_data['db_host'] . ':' . $this->site_data['db_port'] : $this->site_data['db_host']; - $wp_config_create_command = sprintf( 'wp config create --dbuser=\'%s\' --dbname=\'%s\' --dbpass=\'%s\' --dbhost=\'%s\' %s --extra-php="%s"', $this->site_data['db_user'], $this->site_data['db_name'], $this->site_data['db_password'], $db_host, $config_arguments, $extra_php ); + $wp_config_create_command = sprintf( 'wp config create --dbuser=%s --dbname=%s --dbpass=%s --dbhost=%s %s --extra-php="%s"', escapeshellarg( $this->site_data['db_user'] ), escapeshellarg( $this->site_data['db_name'] ), escapeshellarg( $this->site_data['db_password'] ), escapeshellarg( $db_host ), $config_arguments, $extra_php ); try { if ( ! \EE_DOCKER::docker_compose_exec( $wp_config_create_command, 'php', 'bash', 'www-data' ) ) { @@ -1249,9 +1218,9 @@ private function install_wp() { } $prefix = ( $this->site_data['site_ssl'] ) ? 'https://' : 'http://'; - $install_command = sprintf( 'wp core %s --url=\'%s%s\' --title=\'%s\' --admin_user=\'%s\'', $wp_install_command, $prefix, $this->site_data['site_url'], $this->site_data['app_admin_url'], $this->site_data['app_admin_username'] ); - $install_command .= $this->site_data['app_admin_password'] ? sprintf( ' --admin_password=\'%s\'', $this->site_data['app_admin_password'] ) : ''; - $install_command .= sprintf( ' --admin_email=\'%s\' %s', $this->site_data['app_admin_email'], $maybe_multisite_type ); + $install_command = sprintf( 'wp core %s --url=%s --title=%s --admin_user=%s', $wp_install_command, escapeshellarg( $prefix . $this->site_data['site_url'] ), escapeshellarg( $this->site_data['app_admin_url'] ), escapeshellarg( $this->site_data['app_admin_username'] ) ); + $install_command .= $this->site_data['app_admin_password'] ? ' --admin_password=' . escapeshellarg( $this->site_data['app_admin_password'] ) : ''; + $install_command .= sprintf( ' --admin_email=%s %s', escapeshellarg( $this->site_data['app_admin_email'] ), $maybe_multisite_type ); $core_install = \EE_DOCKER::docker_compose_exec( $install_command, 'php', 'bash', 'www-data', '', false, false, [ $this->site_data['app_admin_username'], From 4e9447a60475f11dcea537a988ecd49061aebde1 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 10:54:17 +0530 Subject: [PATCH 3/4] 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-wp working next to an older site-command instead of failing with an undefined function. --- src/WordPress.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/WordPress.php b/src/WordPress.php index 00123ca..6ac2e22 100644 --- a/src/WordPress.php +++ b/src/WordPress.php @@ -278,7 +278,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'] ); + } $mu = \EE\Utils\get_flag_value( $assoc_args, 'mu' ); $this->site_data['app_sub_type'] = $mu ?? 'wp'; From bea73d0e2ed9d2f542cf2501993d9b979898e259 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 11:01:44 +0530 Subject: [PATCH 4/4] fix(site): mask quoted admin credentials in the logged install command The install command now carries the shell-escaped admin values, which differ from the raw values when they contain a quote, so the log masking missed them. Mask the escaped forms first, then the raw ones. --- src/WordPress.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/WordPress.php b/src/WordPress.php index 6ac2e22..5fa061b 100644 --- a/src/WordPress.php +++ b/src/WordPress.php @@ -1225,11 +1225,12 @@ private function install_wp() { $install_command .= $this->site_data['app_admin_password'] ? ' --admin_password=' . escapeshellarg( $this->site_data['app_admin_password'] ) : ''; $install_command .= sprintf( ' --admin_email=%s %s', escapeshellarg( $this->site_data['app_admin_email'] ), $maybe_multisite_type ); - $core_install = \EE_DOCKER::docker_compose_exec( $install_command, 'php', 'bash', 'www-data', '', false, false, [ - $this->site_data['app_admin_username'], - $this->site_data['app_admin_email'], - $this->site_data['app_admin_password'], - ] ); + $secrets = array_filter( [ $this->site_data['app_admin_username'], $this->site_data['app_admin_email'], $this->site_data['app_admin_password'] ], function ( $value ) { + return '' !== (string) $value; + } ); + // The command holds the escaped forms, which differ from the raw values when they contain a quote; mask those first. + $obfuscate = array_merge( array_map( 'escapeshellarg', $secrets ), $secrets ); + $core_install = \EE_DOCKER::docker_compose_exec( $install_command, 'php', 'bash', 'www-data', '', false, false, $obfuscate ); if ( ! $core_install ) { throw new \Exception( 'WordPress install failed. Please check logs.' );