From e3cd197bd643c53d03a5b4a71e34df2efef75b83 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 12 Aug 2026 20:05:24 -0500 Subject: [PATCH 1/7] Slack webhooks: Validate webhook signatures and payload fields at the edge. Slack signature headers and timestamps are format-validated before the constant-time comparison, channel/user/trigger IDs are validated against Slack's ID formats, user names echoed back into responses are stripped of control characters, and token comparisons no longer warn when the token parameter is absent. Each handler documents its actual authentication mechanism (shared token or HMAC signature) in a justified file-level nonce disable, and unslash is disabled only where WordPress genuinely isn't loaded. Co-Authored-By: Claude Fable 5 (cherry picked from commit 6a0883d49e662583a82d2e7509e486d6d87bebd1) --- .../public_html/dotorg/slack/announce.php | 9 ++- .../public_html/dotorg/slack/committers.php | 15 ++++- .../community-deputies-calendly-webhook.php | 10 ++- .../public_html/dotorg/slack/props.php | 3 + .../dotorg/slack/security-team.php | 12 +++- .../public_html/dotorg/slack/subgroup.php | 62 +++++++++++++++++-- .../public_html/dotorg/slack/trac-bot.php | 23 +++++-- 7 files changed, 120 insertions(+), 14 deletions(-) diff --git a/api.wordpress.org/public_html/dotorg/slack/announce.php b/api.wordpress.org/public_html/dotorg/slack/announce.php index 04a7c8918b..9ea2fa7515 100644 --- a/api.wordpress.org/public_html/dotorg/slack/announce.php +++ b/api.wordpress.org/public_html/dotorg/slack/announce.php @@ -9,6 +9,13 @@ require dirname( dirname( __DIR__ ) ) . '/includes/slack/announce/lib.php'; +/* phpcs:disable Generic.WhiteSpace.ScopeIndent -- file-scope code here is historically unindented. + * phpcs:disable WordPress.Security.NonceVerification + * phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash + * Standalone Slack slash-command handler: WordPress is not loaded, so request data is + * never slashed; Slack authenticates with the shared WEBHOOK_TOKEN_* secrets below. + */ + function get_avatar( $username, $slack_id, $team_id ) { global $wpdb; @@ -29,7 +36,7 @@ function get_avatar( $username, $slack_id, $team_id ) { $i = 0; // WEBHOOK_TOKEN_1, WEBHOOK_TOKEN_2, etc. while ( defined( __NAMESPACE__ . '\\WEBHOOK_TOKEN_' . ++$i ) ) { - if ( hash_equals( constant( __NAMESPACE__ . '\\WEBHOOK_TOKEN_' . $i ), $_POST['token'] ) ) { + if ( hash_equals( constant( __NAMESPACE__ . '\\WEBHOOK_TOKEN_' . $i ), $_POST['token'] ?? '' ) ) { run( $_POST ); } } diff --git a/api.wordpress.org/public_html/dotorg/slack/committers.php b/api.wordpress.org/public_html/dotorg/slack/committers.php index e89f6a820b..1c464ad069 100644 --- a/api.wordpress.org/public_html/dotorg/slack/committers.php +++ b/api.wordpress.org/public_html/dotorg/slack/committers.php @@ -6,14 +6,25 @@ require dirname( dirname( __DIR__ ) ) . '/includes/slack-config.php'; -if ( ! hash_equals( WEBHOOK_TOKEN, $_POST['token'] ) ) { +/* + * This is a standalone Slack outgoing-webhook handler: WordPress is not loaded, so request data + * is never slashed. Slack authenticates itself with the shared `WEBHOOK_TOKEN` below; nonces + * don't exist in server-to-server webhooks. + * + * phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + */ + +if ( ! hash_equals( WEBHOOK_TOKEN, $_POST['token'] ?? '' ) ) { return; } +// The Slack user name of whoever triggered the webhook, echoed back in the JSON response below. +$user_name = filter_var( $_POST['user_name'] ?? '', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ); + echo json_encode( array( 'username' => 'wordpressdotorg', 'link_names' => 1, - 'text' => sprintf( '@%s: Use the `/committers` command.', $_POST['user_name'] ), + 'text' => sprintf( '@%s: Use the `/committers` command.', $user_name ), ) ); exit; diff --git a/api.wordpress.org/public_html/dotorg/slack/community-deputies-calendly-webhook.php b/api.wordpress.org/public_html/dotorg/slack/community-deputies-calendly-webhook.php index cb4a7b7e38..1408ee9c4b 100644 --- a/api.wordpress.org/public_html/dotorg/slack/community-deputies-calendly-webhook.php +++ b/api.wordpress.org/public_html/dotorg/slack/community-deputies-calendly-webhook.php @@ -9,6 +9,13 @@ require dirname( __DIR__, 2 ) . '/wp-init.php'; require dirname( __DIR__, 2 ) . '/includes/slack-config.php'; +/* + * Calendly authenticates itself with the shared `COMMUNITY_CALENDLY_SECRET` passed in the webhook + * URL, verified below; nonces don't exist in server-to-server webhooks. + * + * phpcs:disable WordPress.Security.NonceVerification + */ + /** * Quick API wrapper for the Calendly API. */ @@ -40,6 +47,7 @@ function api_request( $url ) { trigger_error( 'The Calendly token has probably been revoked, the password was probably changed.' . 'Please update the COMMUNITY_CALENDLY_TOKEN secrets constant with a new PAT created on https://calendly.com/integrations/api_webhooks from the WordCamp Calendly account.' . + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text PHP error log entry, not HTTP output. wp_remote_retrieve_body( $req ), E_USER_WARNING ); @@ -49,7 +57,7 @@ function api_request( $url ) { } // Check the request is valid. -if ( empty( $_GET['secret'] ) || ! hash_equals( COMMUNITY_CALENDLY_SECRET, $_GET['secret'] ) ) { +if ( empty( $_GET['secret'] ) || ! hash_equals( COMMUNITY_CALENDLY_SECRET, wp_unslash( $_GET['secret'] ) ) ) { header( 'HTTP/1.1 403 Forbidden' ); die( 'Invalid secret provided.' ); } diff --git a/api.wordpress.org/public_html/dotorg/slack/props.php b/api.wordpress.org/public_html/dotorg/slack/props.php index 3025382e1b..f33ee755c7 100644 --- a/api.wordpress.org/public_html/dotorg/slack/props.php +++ b/api.wordpress.org/public_html/dotorg/slack/props.php @@ -40,6 +40,7 @@ header( 'X-Slack-No-Retry', 1 ); // Don't retry this event again. trigger_error( + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text PHP error log entry, not HTTP output. sprintf( 'Received retry for %s because: %s', $message_id, $headers['X-Slack-Retry-Reason'] ), E_USER_NOTICE ); @@ -51,6 +52,7 @@ } } catch ( Exception $exception ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text PHP error log entry, not HTTP output. trigger_error( $exception->getMessage(), E_USER_WARNING ); } finally { @@ -59,6 +61,7 @@ * request, so this should still be 200 even if something goes wrong on our end. */ http_response_code( 200 ); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain-text response body for Slack. die( $result ); } } diff --git a/api.wordpress.org/public_html/dotorg/slack/security-team.php b/api.wordpress.org/public_html/dotorg/slack/security-team.php index 65ce43cf57..b532809793 100644 --- a/api.wordpress.org/public_html/dotorg/slack/security-team.php +++ b/api.wordpress.org/public_html/dotorg/slack/security-team.php @@ -10,6 +10,14 @@ require dirname( dirname( __DIR__ ) ) . '/includes/slack-config.php'; +/* phpcs:disable Generic.WhiteSpace.ScopeIndent -- file-scope code here is historically unindented. + * phpcs:disable WordPress.Security.NonceVerification + * phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash + * Standalone when requested directly: it loads HyperDB but not WordPress, so nothing is + * slashed. (Also included as a library by trac/mentions-handler.php, which skips the + * request handling below.) The Trac server authenticates with the shared API_TOKEN secret. + */ + function slack_api( $method, $content = array() ) { $content['token'] = SLACK_TOKEN; $content = http_build_query( $content ); @@ -76,10 +84,12 @@ function api_call() { exit; } - echo implode( "\n", $team ) . "\n"; // Trailing newline critical. + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- plain-text list; trailing newline critical. + echo implode( "\n", $team ) . "\n"; exit; } +// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- substring comparison only. if ( isset( $_SERVER['REQUEST_URI'] ) && false !== strpos( $_SERVER['REQUEST_URI'], '/security-team.php?token=' ) ) { api_call(); } diff --git a/api.wordpress.org/public_html/dotorg/slack/subgroup.php b/api.wordpress.org/public_html/dotorg/slack/subgroup.php index 61ab079ae3..b77bc590ef 100644 --- a/api.wordpress.org/public_html/dotorg/slack/subgroup.php +++ b/api.wordpress.org/public_html/dotorg/slack/subgroup.php @@ -4,6 +4,15 @@ require dirname( dirname( __DIR__ ) ) . '/includes/slack-config.php'; +/* + * This is a standalone Slack app endpoint: only the object cache is loaded, not WordPress, so + * request data is never slashed. Slack authenticates every request with an HMAC signature over the + * raw request body, verified in `verify_slack_signature()` before anything is dispatched; nonces + * don't exist in server-to-server webhooks. + * + * phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + */ + const CACHE_GROUP = 'api-slack-subgroup'; const CACHE_TTL_ACTIVE_CHANNELS = 120; // Users can create channels outside this app. const CACHE_TTL_ARCHIVED_CHANNELS = 3600; // Archives are slower-moving. @@ -83,8 +92,27 @@ function ack_and_finish() { } function verify_slack_signature( $body ) { - $timestamp = $_SERVER['HTTP_X_SLACK_REQUEST_TIMESTAMP'] ?? ''; - $signature = $_SERVER['HTTP_X_SLACK_SIGNATURE'] ?? ''; + // A Unix timestamp, and a `v0=` prefixed HMAC-SHA256 hex digest. Anything else can't be from Slack. + $timestamp = filter_var( + $_SERVER['HTTP_X_SLACK_REQUEST_TIMESTAMP'] ?? '', + FILTER_VALIDATE_REGEXP, + [ + 'options' => [ + 'regexp' => '/^\d+$/', + 'default' => '', + ], + ] + ); + $signature = filter_var( + $_SERVER['HTTP_X_SLACK_SIGNATURE'] ?? '', + FILTER_VALIDATE_REGEXP, + [ + 'options' => [ + 'regexp' => '/^v0=[0-9a-f]{64}$/', + 'default' => '', + ], + ] + ); if ( ! $timestamp || ! $signature ) { return false; } @@ -104,6 +132,11 @@ function verify_slack_signature( $body ) { // Dispatch: slash command vs. interactivity callback. if ( isset( $_POST['payload'] ) ) { + /* + * A JSON document covered by the verified signature above; sanitizing it would + * corrupt it. Individual fields are validated where they're read. + * phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + */ $payload = json_decode( $_POST['payload'], true ); handle_interaction( $payload ); exit; @@ -111,9 +144,28 @@ function verify_slack_signature( $body ) { handle_slash_command(); function handle_slash_command() { - $channel_id = $_POST['channel_id'] ?? ''; - $user_id = $_POST['user_id'] ?? ''; - $trigger_id = $_POST['trigger_id'] ?? ''; + // Slack channel and user IDs are uppercase alphanumeric, eg. `C0123ABCD` and `U0123ABCD`. + $id_options = [ + 'options' => [ + 'regexp' => '/^[A-Z0-9]+$/', + 'default' => '', + ], + ]; + + $channel_id = filter_var( $_POST['channel_id'] ?? '', FILTER_VALIDATE_REGEXP, $id_options ); + $user_id = filter_var( $_POST['user_id'] ?? '', FILTER_VALIDATE_REGEXP, $id_options ); + + // A trigger ID, eg. `13345224609.738474920.8088930838d88f008e0`. + $trigger_id = filter_var( + $_POST['trigger_id'] ?? '', + FILTER_VALIDATE_REGEXP, + [ + 'options' => [ + 'regexp' => '/^[A-Za-z0-9.]+$/', + 'default' => '', + ], + ] + ); // trigger_id is only valid for 3s, and listing every subgroup's membership will take // longer than that. Open a loading view now, then views.update after we have the data. diff --git a/api.wordpress.org/public_html/dotorg/slack/trac-bot.php b/api.wordpress.org/public_html/dotorg/slack/trac-bot.php index 8fda47cef2..c68276ca79 100644 --- a/api.wordpress.org/public_html/dotorg/slack/trac-bot.php +++ b/api.wordpress.org/public_html/dotorg/slack/trac-bot.php @@ -9,14 +9,28 @@ } namespace Dotorg\Slack\Trac { + /* + * Slack authenticates itself with the shared `URL_SECRET__TRAC_BOT` secret passed in the + * outgoing webhook URL, verified below; nonces don't exist in server-to-server webhooks. + * + * phpcs:disable WordPress.Security.NonceVerification + */ // Verify it came from Slack. - if ( ! hash_equals( URL_SECRET__TRAC_BOT, $_GET['token'] ?? '' ) ) { + if ( ! hash_equals( URL_SECRET__TRAC_BOT, wp_unslash( $_GET['token'] ?? '' ) ) ) { return; } + /* + * The Slack channel name, user name, and message timestamp are interpolated into the Trac + * comment and the message permalink below. + */ + $channel_name = sanitize_text_field( wp_unslash( $_POST['channel_name'] ?? '' ) ); + $user_name = sanitize_text_field( wp_unslash( $_POST['user_name'] ?? '' ) ); + $msg_timestamp = sanitize_text_field( wp_unslash( $_POST['timestamp'] ?? '' ) ); + // Prevent recursion. - if ( $_POST['user_name'] === 'slackbot' ) { + if ( 'slackbot' === $user_name ) { return; } @@ -99,7 +113,7 @@ $slack->send( $parser->get_channel(), $parser->get_thread() ); - if ( $_POST['channel_name'] === 'test' ) { + if ( 'test' === $channel_name ) { // Don't post to Trac if we're coming from #test. continue; } @@ -111,7 +125,8 @@ $trac_xmlrpc = new \Trac( 'slackbot', SLACKBOT_WPORG_PASSWORD, "https://$trac.trac.wordpress.org/login/xmlrpc" ); - $comment = sprintf( $comment_template, $_POST['channel_name'], $_POST['user_name'], str_replace( '.', '', $_POST['timestamp'] ) ); + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- local to this webhook script. + $comment = sprintf( $comment_template, $channel_name, $user_name, str_replace( '.', '', $msg_timestamp ) ); foreach ( $results['ticket'] as $ticket ) { $ticket_id = is_array( $ticket ) ? $ticket['id'] : $ticket; From 1184aa30cb3b1d48272214c41d3e69327fcbf3a6 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 12 Aug 2026 20:41:53 -0500 Subject: [PATCH 2/7] Slack webhooks: Guard token comparisons against array input; use statement namespaces. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An array parameter (`token[]`) reaching hash_equals() throws a TypeError and turns an invalid request into a 500; every token/secret comparison now verifies the value is a string first. announce.php and security-team.php also move from brace-namespace syntax to statement form — their global-namespace blocks only held requires, which behave identically inside the named namespace — making their unindented file-scope code correct and dropping the ScopeIndent disable that the brace form required. The subgroup payload ignore is restored to single-line form, since an annotation inside a multi-line block comment does not apply to the code following the comment. Co-Authored-By: Claude Fable 5 --- .../public_html/dotorg/slack/announce.php | 31 +++++++++---------- .../public_html/dotorg/slack/committers.php | 3 +- .../community-deputies-calendly-webhook.php | 2 +- .../dotorg/slack/security-team.php | 21 +++++-------- .../public_html/dotorg/slack/subgroup.php | 6 +--- .../public_html/dotorg/slack/trac-bot.php | 4 +-- 6 files changed, 29 insertions(+), 38 deletions(-) diff --git a/api.wordpress.org/public_html/dotorg/slack/announce.php b/api.wordpress.org/public_html/dotorg/slack/announce.php index 9ea2fa7515..28b307cf68 100644 --- a/api.wordpress.org/public_html/dotorg/slack/announce.php +++ b/api.wordpress.org/public_html/dotorg/slack/announce.php @@ -1,19 +1,16 @@ Date: Wed, 12 Aug 2026 20:57:10 -0500 Subject: [PATCH 3/7] Slack webhooks: Use the two-argument dirname() form in the restructured requires. Co-Authored-By: Claude Fable 5 --- api.wordpress.org/public_html/dotorg/slack/announce.php | 6 +++--- .../public_html/dotorg/slack/security-team.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api.wordpress.org/public_html/dotorg/slack/announce.php b/api.wordpress.org/public_html/dotorg/slack/announce.php index 28b307cf68..39fdc7084f 100644 --- a/api.wordpress.org/public_html/dotorg/slack/announce.php +++ b/api.wordpress.org/public_html/dotorg/slack/announce.php @@ -1,9 +1,9 @@ Date: Wed, 12 Aug 2026 20:57:47 -0500 Subject: [PATCH 4/7] Slack webhooks: Drop change-justification comments from the token guards. Co-Authored-By: Claude Fable 5 --- api.wordpress.org/public_html/dotorg/slack/announce.php | 1 - api.wordpress.org/public_html/dotorg/slack/committers.php | 1 - api.wordpress.org/public_html/dotorg/slack/security-team.php | 2 +- api.wordpress.org/public_html/dotorg/slack/trac-bot.php | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/api.wordpress.org/public_html/dotorg/slack/announce.php b/api.wordpress.org/public_html/dotorg/slack/announce.php index 39fdc7084f..e65a9dad5e 100644 --- a/api.wordpress.org/public_html/dotorg/slack/announce.php +++ b/api.wordpress.org/public_html/dotorg/slack/announce.php @@ -30,7 +30,6 @@ function get_avatar( $username, $slack_id, $team_id ) { return sprintf( 'https://secure.gravatar.com/avatar/%s?s=96d=mm&r=G&%s', $hash, time() ); } -// An array (`token[]`) would make hash_equals() throw; bail on anything but a string. if ( ! is_string( $_POST['token'] ?? null ) ) { return; } diff --git a/api.wordpress.org/public_html/dotorg/slack/committers.php b/api.wordpress.org/public_html/dotorg/slack/committers.php index b45a93ed83..980e1a38be 100644 --- a/api.wordpress.org/public_html/dotorg/slack/committers.php +++ b/api.wordpress.org/public_html/dotorg/slack/committers.php @@ -14,7 +14,6 @@ * phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.MissingUnslash */ -// An array (`token[]`) would make hash_equals() throw; treat it as an invalid token. if ( ! is_string( $_POST['token'] ?? null ) || ! hash_equals( WEBHOOK_TOKEN, $_POST['token'] ) ) { return; } diff --git a/api.wordpress.org/public_html/dotorg/slack/security-team.php b/api.wordpress.org/public_html/dotorg/slack/security-team.php index cc781ceb3c..8cf2b1886f 100644 --- a/api.wordpress.org/public_html/dotorg/slack/security-team.php +++ b/api.wordpress.org/public_html/dotorg/slack/security-team.php @@ -66,7 +66,7 @@ function get_security_team( $user_field = 'user_login' ) { function api_call() { header( 'Content-type: text/plain' ); - // Confirm it came from the Trac server. An array (`token[]`) would make hash_equals() throw. + // Confirm it came from the Trac server. if ( ! is_string( $_GET['token'] ?? null ) || ! hash_equals( API_TOKEN, $_GET['token'] ) ) { exit; } diff --git a/api.wordpress.org/public_html/dotorg/slack/trac-bot.php b/api.wordpress.org/public_html/dotorg/slack/trac-bot.php index 3aea48739d..d3e598ec42 100644 --- a/api.wordpress.org/public_html/dotorg/slack/trac-bot.php +++ b/api.wordpress.org/public_html/dotorg/slack/trac-bot.php @@ -16,7 +16,7 @@ * phpcs:disable WordPress.Security.NonceVerification */ - // Verify it came from Slack. An array (`token[]`) would make hash_equals() throw. + // Verify it came from Slack. if ( ! is_string( $_GET['token'] ?? null ) || ! hash_equals( URL_SECRET__TRAC_BOT, wp_unslash( $_GET['token'] ) ) ) { return; } From 6cff98e83182ac7a235f761198ef9674afa8f935 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 12 Aug 2026 21:02:52 -0500 Subject: [PATCH 5/7] Slack webhooks: Add file docblocks carrying the phpcs justifications. Co-Authored-By: Claude Fable 5 --- .../public_html/dotorg/slack/announce.php | 18 ++++++++++------- .../public_html/dotorg/slack/committers.php | 19 ++++++++++-------- .../community-deputies-calendly-webhook.php | 18 ++++++++++------- .../dotorg/slack/security-team.php | 20 +++++++++++-------- .../public_html/dotorg/slack/subgroup.php | 15 ++++++++------ .../public_html/dotorg/slack/trac-bot.php | 16 +++++++++------ 6 files changed, 64 insertions(+), 42 deletions(-) diff --git a/api.wordpress.org/public_html/dotorg/slack/announce.php b/api.wordpress.org/public_html/dotorg/slack/announce.php index e65a9dad5e..3f87ea45ea 100644 --- a/api.wordpress.org/public_html/dotorg/slack/announce.php +++ b/api.wordpress.org/public_html/dotorg/slack/announce.php @@ -1,18 +1,22 @@ Date: Wed, 12 Aug 2026 21:27:22 -0500 Subject: [PATCH 6/7] Slack webhooks: Rename the Trac comment variable and harden filter fallbacks. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With WordPress loaded, trac-bot's file scope is global scope, so $comment was overwriting core's global of the same name — renamed to $trac_comment instead of suppressing the sniff with an inaccurate justification. The filter_var 'default' option doesn't apply to array input (it returns false), so the subgroup ID lookups and the committers user name are cast to string to make the intended empty-string fallback hold. Co-Authored-By: Claude Fable 5 --- api.wordpress.org/public_html/dotorg/slack/committers.php | 2 +- api.wordpress.org/public_html/dotorg/slack/subgroup.php | 6 +++--- api.wordpress.org/public_html/dotorg/slack/trac-bot.php | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/api.wordpress.org/public_html/dotorg/slack/committers.php b/api.wordpress.org/public_html/dotorg/slack/committers.php index 543282f0f2..10e06efaa2 100644 --- a/api.wordpress.org/public_html/dotorg/slack/committers.php +++ b/api.wordpress.org/public_html/dotorg/slack/committers.php @@ -22,7 +22,7 @@ } // The Slack user name of whoever triggered the webhook, echoed back in the JSON response below. -$user_name = filter_var( $_POST['user_name'] ?? '', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ); +$user_name = (string) filter_var( $_POST['user_name'] ?? '', FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW ); echo json_encode( array( 'username' => 'wordpressdotorg', diff --git a/api.wordpress.org/public_html/dotorg/slack/subgroup.php b/api.wordpress.org/public_html/dotorg/slack/subgroup.php index 64c0ed6979..69ae3155d2 100644 --- a/api.wordpress.org/public_html/dotorg/slack/subgroup.php +++ b/api.wordpress.org/public_html/dotorg/slack/subgroup.php @@ -151,11 +151,11 @@ function handle_slash_command() { ], ]; - $channel_id = filter_var( $_POST['channel_id'] ?? '', FILTER_VALIDATE_REGEXP, $id_options ); - $user_id = filter_var( $_POST['user_id'] ?? '', FILTER_VALIDATE_REGEXP, $id_options ); + $channel_id = (string) filter_var( $_POST['channel_id'] ?? '', FILTER_VALIDATE_REGEXP, $id_options ); + $user_id = (string) filter_var( $_POST['user_id'] ?? '', FILTER_VALIDATE_REGEXP, $id_options ); // A trigger ID, eg. `13345224609.738474920.8088930838d88f008e0`. - $trigger_id = filter_var( + $trigger_id = (string) filter_var( $_POST['trigger_id'] ?? '', FILTER_VALIDATE_REGEXP, [ diff --git a/api.wordpress.org/public_html/dotorg/slack/trac-bot.php b/api.wordpress.org/public_html/dotorg/slack/trac-bot.php index bda2f78b47..345b082c61 100644 --- a/api.wordpress.org/public_html/dotorg/slack/trac-bot.php +++ b/api.wordpress.org/public_html/dotorg/slack/trac-bot.php @@ -129,8 +129,7 @@ $trac_xmlrpc = new \Trac( 'slackbot', SLACKBOT_WPORG_PASSWORD, "https://$trac.trac.wordpress.org/login/xmlrpc" ); - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- local to this webhook script. - $comment = sprintf( $comment_template, $channel_name, $user_name, str_replace( '.', '', $msg_timestamp ) ); + $trac_comment = sprintf( $comment_template, $channel_name, $user_name, str_replace( '.', '', $msg_timestamp ) ); foreach ( $results['ticket'] as $ticket ) { $ticket_id = is_array( $ticket ) ? $ticket['id'] : $ticket; @@ -154,7 +153,7 @@ $parser->set_redundancy( 'trac', $trac, 'ticket', $ticket_id ); - $trac_xmlrpc->ticket_update( $ticket_id, $comment ); + $trac_xmlrpc->ticket_update( $ticket_id, $trac_comment ); } } } From 4bd05a99f309c6e2c44ae763c4184447e8e094a7 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 12 Aug 2026 21:35:52 -0500 Subject: [PATCH 7/7] Slack webhooks: Anchor validation regexes with \z. PCRE's $ end-anchor matches before a trailing newline, letting values with one pass signature, timestamp, and ID validation. Co-Authored-By: Claude Fable 5 --- .../public_html/dotorg/slack/security-team.php | 2 +- .../public_html/dotorg/slack/subgroup.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api.wordpress.org/public_html/dotorg/slack/security-team.php b/api.wordpress.org/public_html/dotorg/slack/security-team.php index c0d33de65c..e94096d080 100644 --- a/api.wordpress.org/public_html/dotorg/slack/security-team.php +++ b/api.wordpress.org/public_html/dotorg/slack/security-team.php @@ -50,7 +50,7 @@ function get_security_team( $user_field = 'user_login' ) { $slack_user_ids = $group['group']['members']; $slack_user_ids = array_filter( $slack_user_ids, function( $user_id ) { - return (bool) preg_match( '/^U[A-Z0-9]+$/', $user_id ); + return (bool) preg_match( '/^U[A-Z0-9]+\z/', $user_id ); }); $slack_user_ids_for_sql = "'" . implode( "', '", $slack_user_ids ) . "'"; $user_ids = $wpdb->get_col( "SELECT user_id FROM slack_users WHERE slack_id IN ($slack_user_ids_for_sql)" ); diff --git a/api.wordpress.org/public_html/dotorg/slack/subgroup.php b/api.wordpress.org/public_html/dotorg/slack/subgroup.php index 69ae3155d2..cc335732af 100644 --- a/api.wordpress.org/public_html/dotorg/slack/subgroup.php +++ b/api.wordpress.org/public_html/dotorg/slack/subgroup.php @@ -101,7 +101,7 @@ function verify_slack_signature( $body ) { FILTER_VALIDATE_REGEXP, [ 'options' => [ - 'regexp' => '/^\d+$/', + 'regexp' => '/^\d+\z/', 'default' => '', ], ] @@ -111,7 +111,7 @@ function verify_slack_signature( $body ) { FILTER_VALIDATE_REGEXP, [ 'options' => [ - 'regexp' => '/^v0=[0-9a-f]{64}$/', + 'regexp' => '/^v0=[0-9a-f]{64}\z/', 'default' => '', ], ] @@ -146,7 +146,7 @@ function handle_slash_command() { // Slack channel and user IDs are uppercase alphanumeric, eg. `C0123ABCD` and `U0123ABCD`. $id_options = [ 'options' => [ - 'regexp' => '/^[A-Z0-9]+$/', + 'regexp' => '/^[A-Z0-9]+\z/', 'default' => '', ], ]; @@ -160,7 +160,7 @@ function handle_slash_command() { FILTER_VALIDATE_REGEXP, [ 'options' => [ - 'regexp' => '/^[A-Za-z0-9.]+$/', + 'regexp' => '/^[A-Za-z0-9.]+\z/', 'default' => '', ], ] @@ -743,7 +743,7 @@ function finalize_create( $new_id, $name, $creator, $parent_id, $parent_name, $u // conversations.invite fails the entire batch on one malformed ID. Filter // defensively so a stray character in a config value can't sink everyone. $valid_invitees = array_values( array_filter( $invitees, function ( $id ) { - return preg_match( '/^[UWB][A-Z0-9]+$/', $id ); + return preg_match( '/^[UWB][A-Z0-9]+\z/', $id ); } ) ); $invitees = $valid_invitees;