diff --git a/api.wordpress.org/public_html/core/browse-happy/1.0/index.php b/api.wordpress.org/public_html/core/browse-happy/1.0/index.php index b307bc2e58..0f9c624f7f 100644 --- a/api.wordpress.org/public_html/core/browse-happy/1.0/index.php +++ b/api.wordpress.org/public_html/core/browse-happy/1.0/index.php @@ -1,38 +1,60 @@ array( + 'regexp' => '/^[a-zA-Z_][a-zA-Z0-9_]*\z/', + 'default' => '', + ), + 'flags' => FILTER_REQUIRE_SCALAR, +); + $jsonp = ''; if ( ! empty( $_GET['jsonp'] ) ) { - $jsonp = preg_replace( '/[^a-zA-Z0-9_]/', '', $_GET['jsonp'] ); + $jsonp = filter_var( $_GET['jsonp'], FILTER_VALIDATE_REGEXP, $jsonp_filter_args ); header( 'Content-Type: application/javascript' ); } else if ( ! empty( $_GET['callback'] ) ) { - $jsonp = preg_replace( '/[^a-zA-Z0-9_]/', '', $_GET['callback'] ); + $jsonp = filter_var( $_GET['callback'], FILTER_VALIDATE_REGEXP, $jsonp_filter_args ); header( 'Content-Type: application/javascript' ); } -if ( empty( $_REQUEST['useragent'] ) ) { +if ( empty( $_REQUEST['useragent'] ) || ! is_string( $_REQUEST['useragent'] ) ) { return; } -$user_agent = $_REQUEST['useragent']; +$user_agent = filter_var( $_REQUEST['useragent'], FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ); $data = browsehappy_parse_user_agent( $user_agent ); // Collect a sample: One out of every 25. -if ( 0 === strpos( $_SERVER['HTTP_USER_AGENT'], 'WordPress/' ) && 1 === rand( 1, 25 ) ) { +// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only used for a prefix comparison, never output or stored. +if ( 0 === strpos( $_SERVER['HTTP_USER_AGENT'] ?? '', 'WordPress/' ) && 1 === rand( 1, 25 ) ) { require( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) . '/includes/hyperdb/bb-10-hyper-db.php' ); bh_record_data( $user_agent, $data ); } if ( $jsonp ) { header( 'Access-Control-Allow-Origin: *' ); - echo $jsonp.'('.json_encode($data).')'; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- validated callback, JSON-encoded payload. + echo $jsonp . '(' . json_encode( $data ) . ')'; } elseif ( defined( 'JSON_RESPONSE' ) ) { header( 'Access-Control-Allow-Origin: *' ); header( 'Content-Type: application/json' ); echo json_encode( $data ); } else { header( 'Content-Type: text/plain' ); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- serialized payload served as text/plain. echo serialize( $data ); } @@ -46,7 +68,12 @@ function bh_record_data( $ua, $data ) { global $wpdb; - list( $wp_ver, $url ) = explode( ';', $_SERVER['HTTP_USER_AGENT'], 2 ); + // The requesting client's own user agent, which is recorded alongside the reported one. + $client_ua = filter_var( $_SERVER['HTTP_USER_AGENT'] ?? '', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ); + + /* Core sends `WordPress/{version}; {site url}`, but the URL may be absent. */ + list( $wp_ver, $url ) = array_pad( explode( ';', $client_ua, 2 ), 2, '' ); + $wp_ver = substr( $wp_ver, 10, 64 ); $url = rtrim( strtolower( trim( $url ) ), '/' ); $pk = md5( $url . '|' . $ua ); diff --git a/api.wordpress.org/public_html/core/browse-happy/1.0/test.php b/api.wordpress.org/public_html/core/browse-happy/1.0/test.php index 18aeb00ce3..c775c8a3e5 100644 --- a/api.wordpress.org/public_html/core/browse-happy/1.0/test.php +++ b/api.wordpress.org/public_html/core/browse-happy/1.0/test.php @@ -1,10 +1,24 @@
"; +/** + * Browse Happy user agent parser test page. + * + * This is a standalone diagnostic page: WordPress is not loaded, so request data is + * never slashed and the `esc_*()` escaping helpers are unavailable. + * + * phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash + * + * @package BrowseHappy + */ include dirname( __FILE__ ) . '/parse.php'; +$user_agent = filter_var( $_SERVER['HTTP_USER_AGENT'] ?? '', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ); + +// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- htmlspecialchars() escapes; no esc_html() here. +echo htmlspecialchars( $user_agent, ENT_QUOTES ) . '

'; -$output = browsehappy_parse_user_agent( $_SERVER['HTTP_USER_AGENT'] ); +$output = browsehappy_parse_user_agent( $user_agent ); -foreach ( $output as $k => $v ) - echo htmlspecialchars( $k . ' = ' . ( is_bool( $v ) ? (int) $v : $v ), ENT_QUOTES ) . "
"; +foreach ( $output as $k => $v ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- htmlspecialchars() escapes; no esc_html() here. + echo htmlspecialchars( $k . ' = ' . ( is_bool( $v ) ? (int) $v : $v ), ENT_QUOTES ) . '
'; +} diff --git a/api.wordpress.org/public_html/core/credits/index.php b/api.wordpress.org/public_html/core/credits/index.php index d554a0e7e5..30cbb05340 100644 --- a/api.wordpress.org/public_html/core/credits/index.php +++ b/api.wordpress.org/public_html/core/credits/index.php @@ -1,4 +1,14 @@ array( + 'regexp' => '/^[0-9][.0-9]*/', + 'default' => '', + ), + ) + ) + ); } elseif ( 'cli' == php_sapi_name() && isset( $argv[1] ) ) { $version = preg_replace( '/^([.0-9]+).*/', '$1', $argv[1] ); } else { $version = WP_CORE_LATEST_RELEASE; } +// A WP locale, e.g. `de_DE_formal` or `es_419`. +$requested_locale = isset( $_GET['locale'] ) ? filter_var( + $_GET['locale'], + FILTER_VALIDATE_REGEXP, + array( + 'options' => array( + 'regexp' => '/^[A-Za-z0-9_-]+\z/', + 'default' => '', + ), + ) +) : ''; + if ( ! is_string( $version ) || version_compare( $version, '3.2', '<' ) || - ( isset( $_GET['locale'] ) && ! is_string( $_GET['locale'] ) ) + ( isset( $_GET['locale'] ) && ! is_string( $requested_locale ) ) ) { header( 'HTTP/1.0 400 Bad Request', true, 400 ); die( 'Bad request.' ); @@ -50,10 +85,13 @@ function like_escape( $text ) { $locale = false; // Convert a locale from a WP locale to a GP locale. -if ( ( isset( $_GET['locale'] ) && 'en_US' != $_GET['locale'] ) || ( 'cli' == php_sapi_name() && isset( $argv[2] ) ) ) { +if ( + ( isset( $_GET['locale'] ) && 'en_US' != $requested_locale ) || + ( 'cli' == php_sapi_name() && isset( $argv[2] ) ) +) { require GLOTPRESS_LOCALES_PATH; - $gp_locale = GP_Locales::by_field( 'wp_locale', isset( $argv[2] ) ? $argv[2] : $_GET['locale'] ); + $gp_locale = GP_Locales::by_field( 'wp_locale', isset( $argv[2] ) ? $argv[2] : $requested_locale ); if ( $gp_locale ) { $locale = $gp_locale; } diff --git a/api.wordpress.org/public_html/core/credits/wp-credits.php b/api.wordpress.org/public_html/core/credits/wp-credits.php index 3638294178..3ba85e0af0 100644 --- a/api.wordpress.org/public_html/core/credits/wp-credits.php +++ b/api.wordpress.org/public_html/core/credits/wp-credits.php @@ -447,6 +447,7 @@ final public function execute() { } elseif ( defined( 'JSON_RESPONSE' ) && JSON_RESPONSE ) { echo json_encode( $results ); } else { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- serialized payload served as text/plain. echo serialize( $results ); } } diff --git a/api.wordpress.org/public_html/core/importers/1.0/index.php b/api.wordpress.org/public_html/core/importers/1.0/index.php index 0258dfa6d9..8bd2df4855 100644 --- a/api.wordpress.org/public_html/core/importers/1.0/index.php +++ b/api.wordpress.org/public_html/core/importers/1.0/index.php @@ -1,4 +1,14 @@ =' ) ) { @@ -21,6 +32,7 @@ } $response = array( 'importers' => $popular_importers, 'translated' => false ); +// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON or serialized payload, not HTML. echo defined( 'JSON_RESPONSE' ) ? json_encode( $response ) : serialize( $response ); function __( $string ) { return $string; } diff --git a/api.wordpress.org/public_html/core/serve-happy/1.0/include.php b/api.wordpress.org/public_html/core/serve-happy/1.0/include.php index 5bcde61f33..481bd6722f 100644 --- a/api.wordpress.org/public_html/core/serve-happy/1.0/include.php +++ b/api.wordpress.org/public_html/core/serve-happy/1.0/include.php @@ -1,4 +1,15 @@ array( + 'regexp' => '#^HTTP/[0-9]+(\.[0-9]+)?\z#', + 'default' => 'HTTP/1.1', + ), + ) + ); $http_code_texts = [ 400 => 'Bad Request', ]; @@ -39,15 +67,28 @@ function output_response( $data ) { header( 'Access-Control-Allow-Origin: *' ); - if ( !empty( $_GET['callback'] ) ) { + // A JSONP callback is a JavaScript identifier, optionally namespaced; anything else is discarded. + $callback = filter_var( + $_GET['callback'] ?? '', + FILTER_VALIDATE_REGEXP, + array( + 'options' => array( + 'regexp' => '/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\z/', + 'default' => '', + ), + 'flags' => FILTER_REQUIRE_SCALAR, + ) + ); + + if ( $callback ) { call_headers( 'application/javascript' ); - echo '/**/' . - preg_replace('/[^a-zA-Z0-9_.]/', '', $_GET['callback'] ) . - '(' . $json_data . ')'; + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- validated callback, JSON-encoded payload. + echo '/**/' . $callback . '(' . $json_data . ')'; } else { call_headers( 'application/json' ); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON-encoded payload served as application/json. echo $json_data; } } \ No newline at end of file diff --git a/api.wordpress.org/public_html/patterns/1.0/index.php b/api.wordpress.org/public_html/patterns/1.0/index.php index 1c838833e7..1528c3a0f7 100644 --- a/api.wordpress.org/public_html/patterns/1.0/index.php +++ b/api.wordpress.org/public_html/patterns/1.0/index.php @@ -1,4 +1,13 @@