From dd3b62b4b89e2e92a23ed02bcc243582a218bbee Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Jul 2026 22:58:13 +0200 Subject: [PATCH 1/6] Harden router.php against path traversal and non-PHP execution --- features/server.feature | 30 ++++++++++++++++++++++++++++++ router.php | 24 +++++++++++++++++------- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/features/server.feature b/features/server.feature index 963720e..cdaf7bd 100644 --- a/features/server.feature +++ b/features/server.feature @@ -73,3 +73,33 @@ Feature: Serve WordPress locally """ https://localhost:8184 """ + + Scenario: Prevent path traversal outside document root + Given a WP install + And I launch in the background `wp server --host=localhost --port=8186` + + When I run `curl -sS --path-as-is http://localhost:8186/%2e%2e/` + Then STDOUT should contain: + """ + Just another WordPress site + """ + + Scenario: Prevent execution of non-PHP extensions + Given a WP install + And a wp-content/uploads/evil.php_.gif file: + """ + GIF89a; + + """ + And I launch in the background `wp server --host=localhost --port=8187` + + When I run `curl -sS http://localhost:8187/wp-content/uploads/evil.php_.gif` + Then STDOUT should contain: + """ + GIF89a; + + """ + And STDOUT should not contain: + """ + FAIL_EXECUTION + """ diff --git a/router.php b/router.php index a0c22df..8bcdd88 100644 --- a/router.php +++ b/router.php @@ -141,26 +141,36 @@ static function ( $buffer ) { // Normalize slashes for file operations $wpcli_server_file = str_replace( array( '/', '\\' ), DIRECTORY_SEPARATOR, $wpcli_server_file ); -if ( file_exists( $wpcli_server_file ) ) { - if ( is_dir( $wpcli_server_file ) && substr( $wpcli_server_path, -1 ) !== '/' ) { +$wpcli_server_real_root = realpath( $wpcli_server_root ); +$wpcli_server_real_file = file_exists( $wpcli_server_file ) ? realpath( $wpcli_server_file ) : false; + +$wpcli_server_is_inside_root = false; +if ( false !== $wpcli_server_real_root && false !== $wpcli_server_real_file ) { + if ( $wpcli_server_real_file === $wpcli_server_real_root || 0 === strpos( $wpcli_server_real_file, rtrim( $wpcli_server_real_root, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR ) ) { + $wpcli_server_is_inside_root = true; + } +} + +if ( $wpcli_server_is_inside_root ) { + if ( is_dir( $wpcli_server_real_file ) && substr( $wpcli_server_path, -1 ) !== '/' ) { header( "Location: $wpcli_server_path/" ); exit; } // Check if this is a PHP file by examining the extension - if ( pathinfo( $wpcli_server_file, PATHINFO_EXTENSION ) === 'php' ) { + if ( 'php' === strtolower( pathinfo( $wpcli_server_real_file, PATHINFO_EXTENSION ) ) ) { // Set $_SERVER variables to mimic direct access to the PHP file $_SERVER['SCRIPT_NAME'] = $wpcli_server_path; $_SERVER['PHP_SELF'] = $wpcli_server_path; - $_SERVER['SCRIPT_FILENAME'] = $wpcli_server_file; + $_SERVER['SCRIPT_FILENAME'] = $wpcli_server_real_file; - chdir( dirname( $wpcli_server_file ) ); - require_once $wpcli_server_file; + chdir( dirname( $wpcli_server_real_file ) ); + require_once $wpcli_server_real_file; } else { return false; } } else { - // File doesn't exist - route to index.php for pretty permalinks + // File doesn't exist or is outside document root - route to index.php for pretty permalinks $_SERVER['SCRIPT_NAME'] = '/index.php'; $_SERVER['PHP_SELF'] = '/index.php'; $_SERVER['SCRIPT_FILENAME'] = $wpcli_server_root . DIRECTORY_SEPARATOR . 'index.php'; From f7f2700c0692eec93eff129fe2864334b1836087 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 29 Jul 2026 09:42:06 +0200 Subject: [PATCH 2/6] Add test case for ..%2f path traversal --- features/server.feature | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/features/server.feature b/features/server.feature index cdaf7bd..bf272c4 100644 --- a/features/server.feature +++ b/features/server.feature @@ -84,6 +84,12 @@ Feature: Serve WordPress locally Just another WordPress site """ + When I run `curl -sS --path-as-is http://localhost:8186/..%2f` + Then STDOUT should contain: + """ + Just another WordPress site + """ + Scenario: Prevent execution of non-PHP extensions Given a WP install And a wp-content/uploads/evil.php_.gif file: From 01340b5070c2825d8d0fdda94dfe71c2935d9a94 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 3 Aug 2026 10:30:48 +0200 Subject: [PATCH 3/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- features/server.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/server.feature b/features/server.feature index bf272c4..367f936 100644 --- a/features/server.feature +++ b/features/server.feature @@ -95,7 +95,7 @@ Feature: Serve WordPress locally And a wp-content/uploads/evil.php_.gif file: """ GIF89a; - + """ And I launch in the background `wp server --host=localhost --port=8187` @@ -103,9 +103,9 @@ Feature: Serve WordPress locally Then STDOUT should contain: """ GIF89a; - + """ And STDOUT should not contain: """ - FAIL_EXECUTION + 1338 """ From 18a90b5a958e1342cc8588107fed4bf91cca831e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 3 Aug 2026 10:31:10 +0200 Subject: [PATCH 4/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- router.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/router.php b/router.php index 8bcdd88..f72b711 100644 --- a/router.php +++ b/router.php @@ -152,8 +152,7 @@ static function ( $buffer ) { } if ( $wpcli_server_is_inside_root ) { - if ( is_dir( $wpcli_server_real_file ) && substr( $wpcli_server_path, -1 ) !== '/' ) { - header( "Location: $wpcli_server_path/" ); + header( 'Location: ' . str_replace( array( "\r", "\n" ), '', $wpcli_server_path ) . '/' ); exit; } From f260fd90f294d36ff794044f8bbe37c4185da087 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 3 Aug 2026 10:37:22 +0200 Subject: [PATCH 5/6] Apply suggestion from @swissspidy --- router.php | 1 + 1 file changed, 1 insertion(+) diff --git a/router.php b/router.php index f72b711..0124c33 100644 --- a/router.php +++ b/router.php @@ -152,6 +152,7 @@ static function ( $buffer ) { } if ( $wpcli_server_is_inside_root ) { + if ( is_dir( $wpcli_server_real_file ) && substr( $wpcli_server_path, -1 ) !== '/' ) { header( 'Location: ' . str_replace( array( "\r", "\n" ), '', $wpcli_server_path ) . '/' ); exit; } From 29bfe211f743f8f7a2ea8a8cd6353f6e596632fe Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 3 Aug 2026 12:32:20 +0200 Subject: [PATCH 6/6] Update test --- features/server.feature | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/server.feature b/features/server.feature index 367f936..badb0f2 100644 --- a/features/server.feature +++ b/features/server.feature @@ -78,16 +78,16 @@ Feature: Serve WordPress locally Given a WP install And I launch in the background `wp server --host=localhost --port=8186` - When I run `curl -sS --path-as-is http://localhost:8186/%2e%2e/` + When I run `curl -sSL --path-as-is http://localhost:8186/%2e%2e/` Then STDOUT should contain: """ - Just another WordPress site + WP CLI Site """ - When I run `curl -sS --path-as-is http://localhost:8186/..%2f` + When I run `curl -sSL --path-as-is http://localhost:8186/..%2f` Then STDOUT should contain: """ - Just another WordPress site + WP CLI Site """ Scenario: Prevent execution of non-PHP extensions