From 0a4616fc3acbc046aef230204c6f59510163a79d Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 12 Aug 2026 20:05:24 -0500 Subject: [PATCH 1/4] Translations API: Sanitize slug and version input. Slugs and versions only feed prepared queries, cache keys, and version comparisons, so control characters are stripped rather than enforcing a format that could reject valid language-pack lookups. Non-scalar input is still rejected with a 400. Replaces raw SERVER_PROTOCOL usage in error responses with http_response_code(). Nonce and unslash sniffs are disabled per file with justification: these are standalone, unauthenticated endpoints where WordPress (and thus slashing and nonces) does not exist. Co-Authored-By: Claude Fable 5 (cherry picked from commit 9a121053234437016566dd01d7c7118520fac1a6) --- .../translations/core/1.0/index.php | 19 ++++++++--- .../translations/plugins/1.0/index.php | 32 ++++++++++++++----- .../translations/themes/1.0/index.php | 32 ++++++++++++++----- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/api.wordpress.org/public_html/translations/core/1.0/index.php b/api.wordpress.org/public_html/translations/core/1.0/index.php index e639ed9216..237e7fc311 100644 --- a/api.wordpress.org/public_html/translations/core/1.0/index.php +++ b/api.wordpress.org/public_html/translations/core/1.0/index.php @@ -1,4 +1,16 @@ $translations ) ); exit; - diff --git a/api.wordpress.org/public_html/translations/plugins/1.0/index.php b/api.wordpress.org/public_html/translations/plugins/1.0/index.php index e840f47706..f7b9036de8 100644 --- a/api.wordpress.org/public_html/translations/plugins/1.0/index.php +++ b/api.wordpress.org/public_html/translations/plugins/1.0/index.php @@ -1,4 +1,16 @@ $translations ) ); exit; - diff --git a/api.wordpress.org/public_html/translations/themes/1.0/index.php b/api.wordpress.org/public_html/translations/themes/1.0/index.php index f23a748eb8..0e7ceb6cdf 100644 --- a/api.wordpress.org/public_html/translations/themes/1.0/index.php +++ b/api.wordpress.org/public_html/translations/themes/1.0/index.php @@ -1,4 +1,16 @@ $translations ) ); exit; - From 7d9658a147bee2f18154c4f979c40389962be6ba Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 12 Aug 2026 20:51:36 -0500 Subject: [PATCH 2/4] Translations API: Fold the phpcs justification comments into the file docblocks. Co-Authored-By: Claude Fable 5 --- .../public_html/translations/core/1.0/index.php | 6 ++---- .../public_html/translations/plugins/1.0/index.php | 6 ++---- .../public_html/translations/themes/1.0/index.php | 6 ++---- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/api.wordpress.org/public_html/translations/core/1.0/index.php b/api.wordpress.org/public_html/translations/core/1.0/index.php index 237e7fc311..3d2ca7dec0 100644 --- a/api.wordpress.org/public_html/translations/core/1.0/index.php +++ b/api.wordpress.org/public_html/translations/core/1.0/index.php @@ -2,14 +2,12 @@ /** * WordPress.org Translations API endpoint for WordPress core. * - * @package WordPressdotorg\API\Translations - */ - -/* * This is a standalone, unauthenticated, stateless API endpoint: WordPress is not loaded, * so request data is never slashed, and there is no session or nonce infrastructure. * * phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + * + * @package WordPressdotorg\API\Translations */ $base_dir = dirname( dirname( dirname( __DIR__ ) ) ); diff --git a/api.wordpress.org/public_html/translations/plugins/1.0/index.php b/api.wordpress.org/public_html/translations/plugins/1.0/index.php index f7b9036de8..c1f1b4d766 100644 --- a/api.wordpress.org/public_html/translations/plugins/1.0/index.php +++ b/api.wordpress.org/public_html/translations/plugins/1.0/index.php @@ -2,14 +2,12 @@ /** * WordPress.org Translations API endpoint for plugins. * - * @package WordPressdotorg\API\Translations - */ - -/* * This is a standalone, unauthenticated, stateless API endpoint: WordPress is not loaded, * so request data is never slashed, and there is no session or nonce infrastructure. * * phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + * + * @package WordPressdotorg\API\Translations */ $base_dir = dirname( dirname( dirname( __DIR__ ) ) ); diff --git a/api.wordpress.org/public_html/translations/themes/1.0/index.php b/api.wordpress.org/public_html/translations/themes/1.0/index.php index 0e7ceb6cdf..e5b5b7ac43 100644 --- a/api.wordpress.org/public_html/translations/themes/1.0/index.php +++ b/api.wordpress.org/public_html/translations/themes/1.0/index.php @@ -2,14 +2,12 @@ /** * WordPress.org Translations API endpoint for themes. * - * @package WordPressdotorg\API\Translations - */ - -/* * This is a standalone, unauthenticated, stateless API endpoint: WordPress is not loaded, * so request data is never slashed, and there is no session or nonce infrastructure. * * phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + * + * @package WordPressdotorg\API\Translations */ $base_dir = dirname( dirname( dirname( __DIR__ ) ) ); From e187c9a020a7aa1061f31eb05a68407c3bbc6c57 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 12 Aug 2026 21:14:56 -0500 Subject: [PATCH 3/4] Translations API: Validate slug and version against a cache-key-safe charset. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FILTER_FLAG_STRIP_LOW left spaces and high bytes intact, and those values flow into memcached cache keys, which reject them — so a request like ?version=1%20x bypassed the cache entirely and hit the database on every request. Values outside [A-Za-z0-9._-] now get the existing 400 response instead. Co-Authored-By: Claude Fable 5 --- .../translations/core/1.0/index.php | 11 ++++++++- .../translations/plugins/1.0/index.php | 23 +++++++++++++++---- .../translations/themes/1.0/index.php | 23 +++++++++++++++---- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/api.wordpress.org/public_html/translations/core/1.0/index.php b/api.wordpress.org/public_html/translations/core/1.0/index.php index 3d2ca7dec0..89edfa5436 100644 --- a/api.wordpress.org/public_html/translations/core/1.0/index.php +++ b/api.wordpress.org/public_html/translations/core/1.0/index.php @@ -19,7 +19,16 @@ $version = WP_CORE_LATEST_RELEASE; if ( isset( $_REQUEST['version'] ) ) { - $version = filter_var( $_REQUEST['version'], FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ); + // This becomes a memcached cache key, which rejects spaces and control characters. + $version = filter_var( + $_REQUEST['version'], + FILTER_VALIDATE_REGEXP, + array( + 'options' => array( + 'regexp' => '/^[0-9][a-z0-9._-]{0,99}$/i', + ), + ) + ); if ( empty( $version ) || ! is_string( $version ) || ! is_numeric( $version[0] ) ) { http_response_code( 400 ); die( '?version= must be a valid WordPress version' ); diff --git a/api.wordpress.org/public_html/translations/plugins/1.0/index.php b/api.wordpress.org/public_html/translations/plugins/1.0/index.php index c1f1b4d766..aea3ebd60c 100644 --- a/api.wordpress.org/public_html/translations/plugins/1.0/index.php +++ b/api.wordpress.org/public_html/translations/plugins/1.0/index.php @@ -17,10 +17,25 @@ require( $base_dir . '/includes/object-cache.php' ); wp_cache_init(); -$slug = isset( $_REQUEST['slug'] ) ? filter_var( $_REQUEST['slug'], FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ) : ''; -$version = isset( $_REQUEST['version'] ) - ? filter_var( $_REQUEST['version'], FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ) - : null; +// These become memcached cache keys, which reject spaces and control characters. +$slug = isset( $_REQUEST['slug'] ) ? filter_var( + $_REQUEST['slug'], + FILTER_VALIDATE_REGEXP, + array( + 'options' => array( + 'regexp' => '/^[a-z0-9._-]{1,100}$/i', + ), + ) +) : ''; +$version = isset( $_REQUEST['version'] ) ? filter_var( + $_REQUEST['version'], + FILTER_VALIDATE_REGEXP, + array( + 'options' => array( + 'regexp' => '/^[a-z0-9._-]{1,100}$/i', + ), + ) +) : null; if ( isset( $_REQUEST['slug'] ) && ! is_string( $slug ) ) { http_response_code( 400 ); diff --git a/api.wordpress.org/public_html/translations/themes/1.0/index.php b/api.wordpress.org/public_html/translations/themes/1.0/index.php index e5b5b7ac43..e187d7aef0 100644 --- a/api.wordpress.org/public_html/translations/themes/1.0/index.php +++ b/api.wordpress.org/public_html/translations/themes/1.0/index.php @@ -17,10 +17,25 @@ require( $base_dir . '/includes/object-cache.php' ); wp_cache_init(); -$slug = isset( $_REQUEST['slug'] ) ? filter_var( $_REQUEST['slug'], FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ) : ''; -$version = isset( $_REQUEST['version'] ) - ? filter_var( $_REQUEST['version'], FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ) - : null; +// These become memcached cache keys, which reject spaces and control characters. +$slug = isset( $_REQUEST['slug'] ) ? filter_var( + $_REQUEST['slug'], + FILTER_VALIDATE_REGEXP, + array( + 'options' => array( + 'regexp' => '/^[a-z0-9._-]{1,100}$/i', + ), + ) +) : ''; +$version = isset( $_REQUEST['version'] ) ? filter_var( + $_REQUEST['version'], + FILTER_VALIDATE_REGEXP, + array( + 'options' => array( + 'regexp' => '/^[a-z0-9._-]{1,100}$/i', + ), + ) +) : null; if ( isset( $_REQUEST['slug'] ) && ! is_string( $slug ) ) { http_response_code( 400 ); From bf8724b5f0e39d2ef59107fbe9582a9eb8f61ba8 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 12 Aug 2026 21:34:19 -0500 Subject: [PATCH 4/4] Translations API: Anchor validation regexes with \z. PCRE's $ end-anchor matches before a trailing newline, which would have let a newline back into the memcached cache keys these values were just validated for. Co-Authored-By: Claude Fable 5 --- api.wordpress.org/public_html/translations/core/1.0/index.php | 2 +- .../public_html/translations/plugins/1.0/index.php | 4 ++-- .../public_html/translations/themes/1.0/index.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api.wordpress.org/public_html/translations/core/1.0/index.php b/api.wordpress.org/public_html/translations/core/1.0/index.php index 89edfa5436..0333319a2b 100644 --- a/api.wordpress.org/public_html/translations/core/1.0/index.php +++ b/api.wordpress.org/public_html/translations/core/1.0/index.php @@ -25,7 +25,7 @@ FILTER_VALIDATE_REGEXP, array( 'options' => array( - 'regexp' => '/^[0-9][a-z0-9._-]{0,99}$/i', + 'regexp' => '/^[0-9][a-z0-9._-]{0,99}\z/i', ), ) ); diff --git a/api.wordpress.org/public_html/translations/plugins/1.0/index.php b/api.wordpress.org/public_html/translations/plugins/1.0/index.php index aea3ebd60c..6998ac96ea 100644 --- a/api.wordpress.org/public_html/translations/plugins/1.0/index.php +++ b/api.wordpress.org/public_html/translations/plugins/1.0/index.php @@ -23,7 +23,7 @@ FILTER_VALIDATE_REGEXP, array( 'options' => array( - 'regexp' => '/^[a-z0-9._-]{1,100}$/i', + 'regexp' => '/^[a-z0-9._-]{1,100}\z/i', ), ) ) : ''; @@ -32,7 +32,7 @@ FILTER_VALIDATE_REGEXP, array( 'options' => array( - 'regexp' => '/^[a-z0-9._-]{1,100}$/i', + 'regexp' => '/^[a-z0-9._-]{1,100}\z/i', ), ) ) : null; diff --git a/api.wordpress.org/public_html/translations/themes/1.0/index.php b/api.wordpress.org/public_html/translations/themes/1.0/index.php index e187d7aef0..30f9cc7f9d 100644 --- a/api.wordpress.org/public_html/translations/themes/1.0/index.php +++ b/api.wordpress.org/public_html/translations/themes/1.0/index.php @@ -23,7 +23,7 @@ FILTER_VALIDATE_REGEXP, array( 'options' => array( - 'regexp' => '/^[a-z0-9._-]{1,100}$/i', + 'regexp' => '/^[a-z0-9._-]{1,100}\z/i', ), ) ) : ''; @@ -32,7 +32,7 @@ FILTER_VALIDATE_REGEXP, array( 'options' => array( - 'regexp' => '/^[a-z0-9._-]{1,100}$/i', + 'regexp' => '/^[a-z0-9._-]{1,100}\z/i', ), ) ) : null;