From 7a6be2aa59f84b01459ddc502917e17dd12d3c2c Mon Sep 17 00:00:00 2001 From: Andrew Shell Date: Fri, 15 May 2026 08:23:21 -0500 Subject: [PATCH 1/5] Revert cloud element to original port-80 / http-post logic Commit a541e55 made the RSS element advertise port='443' for https sites, which breaks FeedLand (the largest rssCloud consumer). Revert only the cloud element output to the original logic: explicit home port if present, otherwise 80, with protocol='http-post'. The notification-request 443->https subscriber logic and the non-standard port whitelist are intentionally kept. Co-Authored-By: Claude Opus 4.7 (1M context) --- rsscloud/readme.txt | 1 - rsscloud/rsscloud.php | 6 +----- tests/test-rsscloud.php | 18 ------------------ 3 files changed, 1 insertion(+), 24 deletions(-) diff --git a/rsscloud/readme.txt b/rsscloud/readme.txt index 9a45ff2..941a982 100644 --- a/rsscloud/readme.txt +++ b/rsscloud/readme.txt @@ -19,7 +19,6 @@ Adds RSSCloud ( http://rsscloud.co/ ) capabilities to your RSS feed. * Allow notifications to https subscribers if port 443 is specified * More reliable detection of failed notification requests * Reject subscription requests with no domain when the remote address is unavailable -* Use port 443 in cloud tag when site is https = 0.5.0 = * Updates to support PHP 8+ diff --git a/rsscloud/rsscloud.php b/rsscloud/rsscloud.php index 5cae602..ff7686e 100644 --- a/rsscloud/rsscloud.php +++ b/rsscloud/rsscloud.php @@ -75,11 +75,7 @@ function rsscloud_add_rss_cloud_element( ) { $cloud = parse_url( get_option( 'home' ) . '/?rsscloud=notify' ); - if ( isset( $cloud['port'] ) ) { - $cloud['port'] = (int) $cloud['port']; - } else { - $cloud['port'] = ( isset( $cloud['scheme'] ) && 'https' === $cloud['scheme'] ) ? 443 : 80; - } + $cloud['port'] = isset( $cloud['port'] ) ? (int) $cloud['port'] : 80; $cloud['path'] .= "?{$cloud['query']}"; diff --git a/tests/test-rsscloud.php b/tests/test-rsscloud.php index 939af50..5ca46a1 100644 --- a/tests/test-rsscloud.php +++ b/tests/test-rsscloud.php @@ -76,24 +76,6 @@ public function test_add_rss_cloud_element_uses_port_from_home_url() { $this->assertStringContainsString( "port='" . $port . "'", $output ); } - public function test_add_rss_cloud_element_defaults_port_to_443_for_https_home() { - $this->go_to( get_feed_link( 'rss2' ) ); - - // Override home AFTER go_to so is_feed() still works. - add_filter( - 'option_home', - function () { - return 'https://example.com'; - } - ); - - ob_start(); - rsscloud_add_rss_cloud_element(); - $output = ob_get_clean(); - - $this->assertStringContainsString( "port='443'", $output ); - } - public function test_add_rss_cloud_element_escapes_attribute_values() { $this->go_to( get_feed_link( 'rss2' ) ); From e10ebad95c23e13e3e25faa3df376a42d42008f2 Mon Sep 17 00:00:00 2001 From: Andrew Shell Date: Fri, 7 Aug 2026 09:22:24 -0500 Subject: [PATCH 2/5] Remove http_allowed_safe_ports whitelist The whitelist added in d05bd40 let wp_safe_remote_*() reach subscribers on non-standard ports. It won't achieve that on WordPress.com, where outbound requests are restricted by firewall rules independent of WordPress's own URL validation, so it adds complexity without reliably delivering the capability. Remove the filter from both the notification request and post notification paths, along with the tests and changelog entry documenting it. Subscribers on non-standard ports are no longer supported; ports 80 and 443 continue to work, as both are in WordPress's default allowed ports. The wp_safe_remote_*() calls introduced alongside the whitelist are kept, so that SSRF protection survives independently of the port workaround. Co-Authored-By: Claude Opus 5 (1M context) --- rsscloud/notification-request.php | 8 ------ rsscloud/readme.txt | 1 - rsscloud/send-post-notifications.php | 8 ------ tests/test-notification-request.php | 36 -------------------------- tests/test-send-post-notifications.php | 33 ----------------------- 5 files changed, 86 deletions(-) diff --git a/rsscloud/notification-request.php b/rsscloud/notification-request.php index 0eecf06..2690c4e 100644 --- a/rsscloud/notification-request.php +++ b/rsscloud/notification-request.php @@ -44,12 +44,6 @@ function rsscloud_hub_process_notification_request( ) { $scheme = ( 443 === $port ) ? 'https://' : 'http://'; - $allow_port = function ( $ports ) use ( $port ) { - $ports[] = $port; - return $ports; - }; - add_filter( 'http_allowed_safe_ports', $allow_port ); - if ( !empty( $_POST['domain'] ) ) { $domain = str_replace( '@', '', sanitize_text_field( wp_unslash( $_POST['domain'] ) ) ); $notify_url = $domain . ':' . $port . $path; @@ -66,8 +60,6 @@ function rsscloud_hub_process_notification_request( ) { $result = wp_safe_remote_post( $notify_url, array( 'method' => 'POST', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, 'body' => array( 'url' => esc_url_raw( wp_unslash( $_POST['url1'] ) ) ) ) ); } - remove_filter( 'http_allowed_safe_ports', $allow_port ); - if ( is_wp_error( $result ) ) rsscloud_notify_result( 'false', 'Error testing notification URL : ' . $result->get_error_message() ); diff --git a/rsscloud/readme.txt b/rsscloud/readme.txt index 941a982..a7ef07a 100644 --- a/rsscloud/readme.txt +++ b/rsscloud/readme.txt @@ -15,7 +15,6 @@ Adds RSSCloud ( http://rsscloud.co/ ) capabilities to your RSS feed. = 0.5.1 = * Harden plugin files against direct access and escape values in the cloud element output -* Allow notifications to subscribers using non-standard ports * Allow notifications to https subscribers if port 443 is specified * More reliable detection of failed notification requests * Reject subscription requests with no domain when the remote address is unavailable diff --git a/rsscloud/send-post-notifications.php b/rsscloud/send-post-notifications.php index c9ba56a..63638ba 100644 --- a/rsscloud/send-post-notifications.php +++ b/rsscloud/send-post-notifications.php @@ -30,16 +30,8 @@ function rsscloud_send_post_notifications( $rss2_url = false ) { if ( !empty( $url['port'] ) ) $port = $url['port']; - $allow_port = function ( $ports ) use ( $port ) { - $ports[] = (int) $port; - return $ports; - }; - add_filter( 'http_allowed_safe_ports', $allow_port ); - $result = wp_safe_remote_post( $notify_url, array( 'method' => 'POST', 'timeout' => RSSCLOUD_HTTP_TIMEOUT, 'user-agent' => RSSCLOUD_USER_AGENT, 'port' => $port, 'body' => array( 'url' => $rss2_url ) ) ); - remove_filter( 'http_allowed_safe_ports', $allow_port ); - do_action( 'rsscloud_send_notification' ); if ( !is_wp_error( $result ) ) diff --git a/tests/test-notification-request.php b/tests/test-notification-request.php index 78305de..626ad3b 100644 --- a/tests/test-notification-request.php +++ b/tests/test-notification-request.php @@ -535,42 +535,6 @@ function ( $preempt, $args, $url ) { $this->assertArrayHasKey( 'http://callback.example.com:9000/notify', $notify[ $this->feed_url ] ); } - public function test_nonstandard_port_is_whitelisted_for_url_validation() { - // No pre_http_request mock — wp_http_validate_url only runs when the - // request isn't short-circuited, and that's where - // http_allowed_safe_ports gets applied. Hook it at priority 999 so - // we observe what the plugin handed to the validator. - $observed_ports = array(); - add_filter( - 'http_allowed_safe_ports', - function ( $ports ) use ( &$observed_ports ) { - $observed_ports = $ports; - return $ports; - }, - 999 - ); - - // Use a TEST-NET-3 IP (RFC 5737) so wp_http_validate_url skips DNS - // and reaches the port check where our filter runs. - $_POST = array( - 'url1' => $this->feed_url, - 'port' => '4000', - 'path' => '/feedupdated', - 'domain' => '203.0.113.5', - ); - - // Request will fail at the network layer (connection refused); we - // don't care about the outcome, only that validation was reached - // with our port whitelisted. - $this->call_process_notification_request(); - - $this->assertContains( - 4000, - $observed_ports, - 'Plugin should add the subscriber port to http_allowed_safe_ports before sending.' - ); - } - public function test_port_443_uses_https_scheme_for_domain_based() { add_filter( 'pre_http_request', diff --git a/tests/test-send-post-notifications.php b/tests/test-send-post-notifications.php index c903628..62115b0 100644 --- a/tests/test-send-post-notifications.php +++ b/tests/test-send-post-notifications.php @@ -274,37 +274,4 @@ function () use ( &$fired ) { $this->assertTrue( $fired ); } - - public function test_nonstandard_port_is_whitelisted_for_url_validation() { - // No pre_http_request mock — we want wp_http_validate_url to run so - // the plugin's http_allowed_safe_ports filter gets applied. Hook at - // priority 999 to observe what the validator sees. - $observed_ports = array(); - add_filter( - 'http_allowed_safe_ports', - function ( $ports ) use ( &$observed_ports ) { - $observed_ports = $ports; - return $ports; - }, - 999 - ); - - // TEST-NET-3 IP (RFC 5737) — wp_http_validate_url skips DNS for - // literal IPs and reaches the port check where our filter fires. - $this->set_notifications( - $this->build_notifications( - array( - 'notify_url' => 'http://203.0.113.5:4000/feedupdated', - ) - ) - ); - - rsscloud_send_post_notifications( $this->feed_url ); - - $this->assertContains( - 4000, - $observed_ports, - 'Plugin should add the subscriber port to http_allowed_safe_ports before sending.' - ); - } } From 7c7b27d698bf4a45aee055f52189c0b51daf8c86 Mon Sep 17 00:00:00 2001 From: Andrew Shell Date: Fri, 7 Aug 2026 09:42:45 -0500 Subject: [PATCH 3/5] Bump minimum WordPress version to 3.6 Plugin check fails against the declared minimum of 2.8: wp_unslash() and wp_safe_remote_post() both require WordPress 3.6.0. Neither is new here, so this corrects metadata that has been wrong since those calls were introduced. Co-Authored-By: Claude Opus 5 (1M context) --- rsscloud/readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsscloud/readme.txt b/rsscloud/readme.txt index a7ef07a..4470e93 100644 --- a/rsscloud/readme.txt +++ b/rsscloud/readme.txt @@ -1,7 +1,7 @@ === Plugin Name === Contributors: josephscott, automattic Tags: rss -Requires at least: 2.8 +Requires at least: 3.6 Tested up to: 7.0.0 Stable tag: 0.5.1 From 856be0c7c5ac103e7c62ec4924b130a62feb40ce Mon Sep 17 00:00:00 2001 From: Andrew Shell Date: Fri, 7 Aug 2026 09:43:00 -0500 Subject: [PATCH 4/5] Add source:cloud element to the RSS 2.0 feed The cloud element is pinned to port 80 / http-post so FeedLand and other older readers keep working, which leaves it unable to advertise an https endpoint. source:cloud carries the full notification URL as element content, so newer readers get the site's real scheme. Emitting both gives the widest reader support. The namespace is declared on the element itself rather than through the rss2_ns action. Doing it the conventional way collides with any other plugin that binds the source prefix on the rss tag: the WordPress template concatenates rss2_ns callbacks with no separator, so two plugins emitting xmlns:source produce a duplicate attribute and the feed stops parsing. team51-markdown-rss does exactly this, unguarded, for its source:markdown element. Declaring inline also guards against the quieter failure. Namespace declarations are scoped, so a plugin binding the source prefix to a different uri on the rss tag would otherwise make source:cloud resolve to the wrong namespace, and newer readers would skip it with no error and no broken feed. The inner declaration shadows the outer one for this element, which the tests assert against a hostile rss tag. Co-Authored-By: Claude Opus 5 (1M context) --- rsscloud/readme.txt | 1 + rsscloud/rsscloud.php | 18 ++++++- tests/test-rsscloud.php | 109 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/rsscloud/readme.txt b/rsscloud/readme.txt index 4470e93..fa5e697 100644 --- a/rsscloud/readme.txt +++ b/rsscloud/readme.txt @@ -15,6 +15,7 @@ Adds RSSCloud ( http://rsscloud.co/ ) capabilities to your RSS feed. = 0.5.1 = * Harden plugin files against direct access and escape values in the cloud element output +* Add the source:cloud element so newer feed readers get the https notification URL * Allow notifications to https subscribers if port 443 is specified * More reliable detection of failed notification requests * Reject subscription requests with no domain when the remote address is unavailable diff --git a/rsscloud/rsscloud.php b/rsscloud/rsscloud.php index ff7686e..e80d406 100644 --- a/rsscloud/rsscloud.php +++ b/rsscloud/rsscloud.php @@ -23,6 +23,9 @@ if ( !defined( 'RSSCLOUD_HTTP_TIMEOUT' ) ) define( 'RSSCLOUD_HTTP_TIMEOUT', 3 ); +if ( !defined( 'RSSCLOUD_SOURCE_NS' ) ) + define( 'RSSCLOUD_SOURCE_NS', 'https://source.scripting.com/' ); + require dirname( __FILE__ ) . '/data-storage.php'; if ( !function_exists( 'rsscloud_hub_process_notification_request' ) ) @@ -73,7 +76,9 @@ function rsscloud_add_rss_cloud_element( ) { return; } - $cloud = parse_url( get_option( 'home' ) . '/?rsscloud=notify' ); + $notify_url = get_option( 'home' ) . '/?rsscloud=notify'; + + $cloud = parse_url( $notify_url ); $cloud['port'] = isset( $cloud['port'] ) ? (int) $cloud['port'] : 80; @@ -85,6 +90,17 @@ function rsscloud_add_rss_cloud_element( ) { echo " path='" . esc_attr( $cloud['path'] ) . "' registerProcedure=''"; echo " protocol='http-post' />"; echo "\n"; + + // Newer feed readers use source:cloud, which carries the full notification + // URL and so can advertise https where the cloud element above cannot. + // The namespace is declared on the element rather than via the rss2_ns + // action: declaring it here scopes it to this element, so another plugin + // binding the source prefix on the rss tag can neither collide with this + // declaration nor rebind the prefix out from under it. + echo ""; + echo esc_url( $notify_url ); + echo ''; + echo "\n"; } function rsscloud_generate_challenge( $length = 30 ) { diff --git a/tests/test-rsscloud.php b/tests/test-rsscloud.php index 5ca46a1..1b0d52b 100644 --- a/tests/test-rsscloud.php +++ b/tests/test-rsscloud.php @@ -98,6 +98,115 @@ function () { 'Attribute values should use esc_attr() which converts & to &' ); } + public function test_add_rss_cloud_element_outputs_source_cloud_tag() { + $this->go_to( get_feed_link( 'rss2' ) ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'assertStringContainsString( '', $output ); + $this->assertStringContainsString( 'rsscloud=notify', $output ); + } + + public function test_source_cloud_declares_namespace_on_the_element() { + $this->go_to( get_feed_link( 'rss2' ) ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + // The namespace must be declared on the element itself, not left to + // the rss2_ns action, so another plugin binding the source prefix on + // the rss tag cannot collide with it or rebind it. + $this->assertStringContainsString( + "xmlns:source='https://source.scripting.com/'", + $output, + 'source:cloud must carry its own namespace declaration' + ); + } + + public function test_source_cloud_uses_full_url_with_site_scheme() { + $this->go_to( get_feed_link( 'rss2' ) ); + + // Override home AFTER go_to so is_feed() still works. + add_filter( + 'option_home', + function () { + return 'https://secure.example.com'; + } + ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + // The point of source:cloud: it carries the https endpoint that the + // port-80 / http-post cloud element above cannot express. + $this->assertStringContainsString( + '>https://secure.example.com/?rsscloud=notify', + $output + ); + + // The classic cloud element must stay on port 80 / http-post for + // older readers such as FeedLand. + $this->assertStringContainsString( "port='80'", $output ); + $this->assertStringContainsString( "protocol='http-post'", $output ); + } + + public function test_source_cloud_url_is_escaped() { + $this->go_to( get_feed_link( 'rss2' ) ); + + add_filter( + 'option_home', + function () { + return 'https://example.com/path?a=1&b=2'; + } + ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + // Raw & is not well-formed XML in element content. + $this->assertStringNotContainsString( '&b=2', $output, + 'source:cloud URL must be escaped; raw & should not appear' ); + } + + public function test_feed_with_source_cloud_is_well_formed_xml() { + $this->go_to( get_feed_link( 'rss2' ) ); + + ob_start(); + rsscloud_add_rss_cloud_element(); + $output = ob_get_clean(); + + // Wrap the fragment the way feed-rss2.php does, including another + // plugin binding the source prefix to a DIFFERENT uri on the rss tag, + // and confirm the result still parses and resolves correctly. + $xml = '' + . '' + . 't' . $output . ''; + + $prev = libxml_use_internal_errors( true ); + libxml_clear_errors(); + $doc = simplexml_load_string( $xml ); + $errors = libxml_get_errors(); + libxml_clear_errors(); + libxml_use_internal_errors( $prev ); + + $this->assertNotFalse( $doc, 'Feed fragment must be well-formed XML' ); + $this->assertEmpty( $errors, 'Feed fragment must parse without XML errors' ); + + // The inline declaration must win over the rss tag's binding. + $source = $doc->channel->children( 'https://source.scripting.com/' ); + $this->assertNotEmpty( + (string) $source->cloud, + 'source:cloud must resolve to the scripting.com namespace, not the rss tag binding' + ); + $this->assertStringContainsString( 'rsscloud=notify', (string) $source->cloud ); + } + public function test_parse_request_does_nothing_without_rsscloud_var() { $wp = new stdClass(); $wp->query_vars = array( 'p' => '1' ); From ea940c29ee9253ca83ceccf8c00c4b0f417ee224 Mon Sep 17 00:00:00 2001 From: Andrew Shell Date: Fri, 7 Aug 2026 09:48:05 -0500 Subject: [PATCH 5/5] Use wp_strip_all_tags() in rsscloud_notify_result() Raising the minimum to 3.6 in the previous commit made plugin check newly flag these calls: wp_strip_all_tags() arrived in WordPress 2.9.0, so while the declared minimum was 2.8 the check could not recommend it. wp_strip_all_tags() is a safe swap here. It strips script and style content rather than just the tags, and trims the result; both values are short strings that then pass through ent2ncr() and esc_html() before being emitted as XML attributes. Co-Authored-By: Claude Opus 5 (1M context) --- rsscloud/rsscloud.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rsscloud/rsscloud.php b/rsscloud/rsscloud.php index e80d406..9c62c4c 100644 --- a/rsscloud/rsscloud.php +++ b/rsscloud/rsscloud.php @@ -55,11 +55,11 @@ function rsscloud_parse_request( $wp ) { if ( !function_exists( 'rsscloud_notify_result' ) ) { function rsscloud_notify_result( $success, $msg ) { - $success = strip_tags( $success ); + $success = wp_strip_all_tags( $success ); $success = ent2ncr( $success ); $success = esc_html( $success ); - $msg = strip_tags( $msg ); + $msg = wp_strip_all_tags( $msg ); $msg = ent2ncr( $msg ); $msg = esc_html( $msg );