From b393f88573cd2a9e4d590f90ec7ee15774953549 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:54:33 +0000 Subject: [PATCH 1/9] Initial plan From 5cf9c4fbf3e03e481e74cef14c3d8cb07caf818a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:59:58 +0000 Subject: [PATCH 2/9] Remove double escaping in run_ssh_command Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/aliases.feature | 22 ++++++++++++++++++++++ php/WP_CLI/Runner.php | 13 +------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/features/aliases.feature b/features/aliases.feature index 796762432..d30800d3d 100644 --- a/features/aliases.feature +++ b/features/aliases.feature @@ -204,6 +204,28 @@ Feature: Create shortcuts to specific WordPress installs Running SSH command: ssh -i 'identityfile.key' -T -vvv """ + Scenario: SSH commands should not be double-escaped + Given a WP installation in 'foo' + And a wp-cli.yml file: + """ + @foo: + ssh: user@host:/path/to/wordpress + """ + + When I try `wp @foo plugin list --debug` + Then STDERR should contain: + """ + Running SSH command: ssh -T -vvv 'user@host' 'cd '\''/path/to/wordpress'\''; wp plugin list --debug' + """ + And STDERR should not contain: + """ + '\'\''plugin'\'\'' + """ + And STDERR should not contain: + """ + '\'\''list'\'\'' + """ + Scenario: Add an alias Given a WP installation in 'foo' And a wp-cli.yml file: diff --git a/php/WP_CLI/Runner.php b/php/WP_CLI/Runner.php index 5d11b3aa5..21b3cee4a 100644 --- a/php/WP_CLI/Runner.php +++ b/php/WP_CLI/Runner.php @@ -661,18 +661,7 @@ private function run_ssh_command( string $connection_string ): void { } } - $wp_command = $pre_cmd . $env_vars . $wp_binary . ' ' . implode( - ' ', - array_map( - static function ( $arg ): string { - return escapeshellarg( (string) $arg ); }, - $wp_args - ) - ); - - if ( isset( $bits['scheme'] ) && 'docker-compose-run' === $bits['scheme'] ) { - $wp_command = implode( ' ', $wp_args ); - } + $wp_command = $pre_cmd . $env_vars . $wp_binary . ' ' . implode( ' ', $wp_args ); $escaped_command = $this->generate_ssh_command( $bits, $wp_command ); From 5f2fcb1856eda84a75b1d5127ab629c6b653a6c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:01:37 +0000 Subject: [PATCH 3/9] Use minimal quoting for SSH commands - only quote args with special chars Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/aliases.feature | 8 -------- php/WP_CLI/Runner.php | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/features/aliases.feature b/features/aliases.feature index d30800d3d..b875eb595 100644 --- a/features/aliases.feature +++ b/features/aliases.feature @@ -217,14 +217,6 @@ Feature: Create shortcuts to specific WordPress installs """ Running SSH command: ssh -T -vvv 'user@host' 'cd '\''/path/to/wordpress'\''; wp plugin list --debug' """ - And STDERR should not contain: - """ - '\'\''plugin'\'\'' - """ - And STDERR should not contain: - """ - '\'\''list'\'\'' - """ Scenario: Add an alias Given a WP installation in 'foo' diff --git a/php/WP_CLI/Runner.php b/php/WP_CLI/Runner.php index 21b3cee4a..303625a26 100644 --- a/php/WP_CLI/Runner.php +++ b/php/WP_CLI/Runner.php @@ -661,7 +661,22 @@ private function run_ssh_command( string $connection_string ): void { } } - $wp_command = $pre_cmd . $env_vars . $wp_binary . ' ' . implode( ' ', $wp_args ); + // Build command with minimal quoting - only quote arguments that need it. + $escaped_args = []; + foreach ( $wp_args as $arg ) { + $arg_str = (string) $arg; + // Check if argument needs quoting (contains spaces, quotes, or shell metacharacters). + if ( preg_match( '/[\s\'"`$&|;<>(){}\\[\\]!*?~]/', $arg_str ) ) { + $escaped_args[] = escapeshellarg( $arg_str ); + } else { + $escaped_args[] = $arg_str; + } + } + $wp_command = $pre_cmd . $env_vars . $wp_binary . ' ' . implode( ' ', $escaped_args ); + + if ( isset( $bits['scheme'] ) && 'docker-compose-run' === $bits['scheme'] ) { + $wp_command = implode( ' ', $wp_args ); + } $escaped_command = $this->generate_ssh_command( $bits, $wp_command ); From 607eee63468f4cd208d176a7991396ffbf97189d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:03:04 +0000 Subject: [PATCH 4/9] Use conservative regex for safe argument detection Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- php/WP_CLI/Runner.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/php/WP_CLI/Runner.php b/php/WP_CLI/Runner.php index 303625a26..4276fcd92 100644 --- a/php/WP_CLI/Runner.php +++ b/php/WP_CLI/Runner.php @@ -665,11 +665,12 @@ private function run_ssh_command( string $connection_string ): void { $escaped_args = []; foreach ( $wp_args as $arg ) { $arg_str = (string) $arg; - // Check if argument needs quoting (contains spaces, quotes, or shell metacharacters). - if ( preg_match( '/[\s\'"`$&|;<>(){}\\[\\]!*?~]/', $arg_str ) ) { - $escaped_args[] = escapeshellarg( $arg_str ); - } else { + // Only skip quoting for simple alphanumeric arguments (with hyphens, underscores, equals, dots, forward slashes). + // Everything else gets quoted for safety. + if ( '' !== $arg_str && preg_match( '/^[a-zA-Z0-9_=.\/:-]+$/', $arg_str ) ) { $escaped_args[] = $arg_str; + } else { + $escaped_args[] = escapeshellarg( $arg_str ); } } $wp_command = $pre_cmd . $env_vars . $wp_binary . ' ' . implode( ' ', $escaped_args ); From 50e3cf979098520aa66da2fcdc651b48a555785f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:04:03 +0000 Subject: [PATCH 5/9] Add detailed comments explaining the security model Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- php/WP_CLI/Runner.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/php/WP_CLI/Runner.php b/php/WP_CLI/Runner.php index 4276fcd92..513f31078 100644 --- a/php/WP_CLI/Runner.php +++ b/php/WP_CLI/Runner.php @@ -661,11 +661,20 @@ private function run_ssh_command( string $connection_string ): void { } } - // Build command with minimal quoting - only quote arguments that need it. + // Build command with minimal quoting to improve readability in debug output. + // Arguments are only quoted if they contain characters outside the safe set. + // This avoids double-escaping appearance while maintaining security: + // 1. Here: Quote args with special chars for the remote shell + // 2. generate_ssh_command(): Wrap entire command for local shell + // + // Safe characters: alphanumeric, hyphen, underscore, equals, dot, forward slash, colon + // - Forward slash and colon are included because they're common in paths and URLs + // (e.g., --url=https://example.com/path) and are not shell metacharacters + // - All other characters (spaces, quotes, $, &, |, etc.) trigger quoting via escapeshellarg() $escaped_args = []; foreach ( $wp_args as $arg ) { $arg_str = (string) $arg; - // Only skip quoting for simple alphanumeric arguments (with hyphens, underscores, equals, dots, forward slashes). + // Only skip quoting for simple alphanumeric arguments (with hyphens, underscores, equals, dots, forward slashes, colons). // Everything else gets quoted for safety. if ( '' !== $arg_str && preg_match( '/^[a-zA-Z0-9_=.\/:-]+$/', $arg_str ) ) { $escaped_args[] = $arg_str; From 7b14a3870dcc3c968fc7a6d9fa872f7c86394c87 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:06:04 +0000 Subject: [PATCH 6/9] Clarify safety of hyphens in command arguments with additional comments Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- php/WP_CLI/Runner.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/php/WP_CLI/Runner.php b/php/WP_CLI/Runner.php index 513f31078..5fe6d909f 100644 --- a/php/WP_CLI/Runner.php +++ b/php/WP_CLI/Runner.php @@ -668,14 +668,16 @@ private function run_ssh_command( string $connection_string ): void { // 2. generate_ssh_command(): Wrap entire command for local shell // // Safe characters: alphanumeric, hyphen, underscore, equals, dot, forward slash, colon + // - Hyphens (including at start like --debug) are safe because they're part of the + // wp-cli command string that's passed to the remote shell, not SSH options // - Forward slash and colon are included because they're common in paths and URLs // (e.g., --url=https://example.com/path) and are not shell metacharacters // - All other characters (spaces, quotes, $, &, |, etc.) trigger quoting via escapeshellarg() $escaped_args = []; foreach ( $wp_args as $arg ) { $arg_str = (string) $arg; - // Only skip quoting for simple alphanumeric arguments (with hyphens, underscores, equals, dots, forward slashes, colons). - // Everything else gets quoted for safety. + // Quote empty strings and arguments with any characters outside the safe set. + // The empty string check is explicit for clarity, though regex would also catch it. if ( '' !== $arg_str && preg_match( '/^[a-zA-Z0-9_=.\/:-]+$/', $arg_str ) ) { $escaped_args[] = $arg_str; } else { From 87ccd84933112413b2a307a9c2834147f42fc371 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 19 Feb 2026 23:29:37 +0100 Subject: [PATCH 7/9] Update features/aliases.feature Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- features/aliases.feature | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/features/aliases.feature b/features/aliases.feature index b875eb595..b4912c166 100644 --- a/features/aliases.feature +++ b/features/aliases.feature @@ -218,6 +218,19 @@ Feature: Create shortcuts to specific WordPress installs Running SSH command: ssh -T -vvv 'user@host' 'cd '\''/path/to/wordpress'\''; wp plugin list --debug' """ + Scenario: SSH commands correctly escape arguments with spaces + Given a WP installation in 'foo' + And a wp-cli.yml file: + """ + @foo: + ssh: user@host:/path/to/wordpress + """ + + When I try `wp @foo post create --post_title=My Title --debug` + Then STDERR should contain: + """ + Running SSH command: ssh -T -vvv 'user@host' 'cd '\''/path/to/wordpress'\''; wp post create --post_title='\''My Title'\''' + """ Scenario: Add an alias Given a WP installation in 'foo' And a wp-cli.yml file: From cae778e7445d6c0233435f3b11e3d80acd7ebe2a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 23 Feb 2026 19:44:25 +0100 Subject: [PATCH 8/9] Update test --- features/aliases.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/aliases.feature b/features/aliases.feature index 12fc70225..86d413ab1 100644 --- a/features/aliases.feature +++ b/features/aliases.feature @@ -229,8 +229,9 @@ Feature: Create shortcuts to specific WordPress installs When I try `wp @foo post create --post_title=My Title --debug` Then STDERR should contain: """ - Running SSH command: ssh -T -vvv 'user@host' 'cd '\''/path/to/wordpress'\''; wp post create --post_title='\''My Title'\''' + Running SSH command: ssh -T -vvv 'user@host' 'cd '\''/path/to/wordpress'\''; wp post create --post_title=My Title """ + Scenario: Add an alias Given a WP installation in 'foo' And a wp-cli.yml file: From fb69ab6a0893fa3d9a854b3949d709f43535d598 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:13:08 +0000 Subject: [PATCH 9/9] Remove redundant docker-compose-run special case The selective escaping approach now handles docker-compose-run uniformly with other schemes, so the special case that was rebuilding the command without escaping is no longer needed. Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- php/WP_CLI/Runner.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/php/WP_CLI/Runner.php b/php/WP_CLI/Runner.php index 257bf4bbf..1dfce1639 100644 --- a/php/WP_CLI/Runner.php +++ b/php/WP_CLI/Runner.php @@ -735,10 +735,6 @@ private function run_ssh_command( string $connection_string ): void { } $wp_command = $pre_cmd . $env_vars . $wp_binary . ' ' . implode( ' ', $escaped_args ); - if ( isset( $bits['scheme'] ) && 'docker-compose-run' === $bits['scheme'] ) { - $wp_command = implode( ' ', $wp_args ); - } - $escaped_command = $this->generate_ssh_command( $bits, $wp_command ); passthru( $escaped_command, $exit_code );