Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions rsscloud/notification-request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() );

Expand Down
5 changes: 2 additions & 3 deletions rsscloud/readme.txt
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -15,11 +15,10 @@ 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
* 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
* Use port 443 in cloud tag when site is https

= 0.5.0 =
* Updates to support PHP 8+
Expand Down
28 changes: 20 additions & 8 deletions rsscloud/rsscloud.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ) )
Expand Down Expand Up @@ -52,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 );

Expand All @@ -73,13 +76,11 @@ function rsscloud_add_rss_cloud_element( ) {
return;
}

$cloud = parse_url( get_option( 'home' ) . '/?rsscloud=notify' );
$notify_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 = parse_url( $notify_url );

$cloud['port'] = isset( $cloud['port'] ) ? (int) $cloud['port'] : 80;

$cloud['path'] .= "?{$cloud['query']}";

Expand All @@ -89,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 "<source:cloud xmlns:source='" . esc_attr( RSSCLOUD_SOURCE_NS ) . "'>";
echo esc_url( $notify_url );
echo '</source:cloud>';
echo "\n";
}

function rsscloud_generate_challenge( $length = 30 ) {
Expand Down
8 changes: 0 additions & 8 deletions rsscloud/send-post-notifications.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) )
Expand Down
36 changes: 0 additions & 36 deletions tests/test-notification-request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
109 changes: 100 additions & 9 deletions tests/test-rsscloud.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,44 +76,135 @@ 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() {
public function test_add_rss_cloud_element_escapes_attribute_values() {
$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';
return 'http://example.com:8080/path?a=1&b=2';
}
);

ob_start();
rsscloud_add_rss_cloud_element();
$output = ob_get_clean();

$this->assertStringContainsString( "port='443'", $output );
// With proper escaping, '&' in attribute values should become '&amp;'.
$this->assertStringNotContainsString( '&b=2', $output,
'Attribute values must be escaped; raw & should not appear' );
$this->assertStringContainsString( '&amp;', $output,
'Attribute values should use esc_attr() which converts & to &amp;' );
}

public function test_add_rss_cloud_element_escapes_attribute_values() {
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( '<source:cloud ', $output );
$this->assertStringContainsString( '</source:cloud>', $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 'http://example.com:8080/path?a=1&b=2';
return 'https://secure.example.com';
}
);

ob_start();
rsscloud_add_rss_cloud_element();
$output = ob_get_clean();

// With proper escaping, '&' in attribute values should become '&amp;'.
// 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</source:cloud>',
$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,
'Attribute values must be escaped; raw & should not appear' );
$this->assertStringContainsString( '&amp;', $output,
'Attribute values should use esc_attr() which converts & to &amp;' );
'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 = '<?xml version="1.0"?>'
. '<rss version="2.0" xmlns:source="https://example.invalid/other/">'
. '<channel><title>t</title>' . $output . '</channel></rss>';

$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() {
Expand Down
33 changes: 0 additions & 33 deletions tests/test-send-post-notifications.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
);
}
}
Loading