Make WordPress Core

Opened 12 years ago

Last modified 4 weeks ago

#25840 new enhancement

Feature Request: WP_ACCESSIBLE_HOSTS as option

Reported by: xfirefartx's profile xFireFartx Owned by:
Milestone: Future Release Priority: normal
Severity: normal Version: 3.7.1
Component: HTTP API Keywords: has-patch
Focuses: Cc:

Description

Currently WP_ACCESSIBLE_HOSTS is defined as a constant. It would be great if this is a wordpress option (or something equivalent) so you can change it at runtime. If you have a multisite installation and need to add domains to the whitelist you must reload the whole installation to enable them. Writing a simple plugin for this is also not possible since constants can not be redefined.

My suggestion:
1) Store a site_option for mutlisites. This should contain a general whitelist for all blogs
2) Store a option per blog to contain additional whitelists for this single blog
3) Make it configurable via the admin interface (single textbox to enter the domains)
4) In the block_request function (https://github.com/WordPress/WordPress/blob/master/wp-includes/class-http.php#L507) the 2 options should be merged and handled like the constant

This way you could manage the whitelist at runtime.

What do you think about this?

Chris

Attachments (4)

25840.diff (686 bytes) - added by leewillis77 12 years ago.
Proposed patch
25840.1.diff (909 bytes) - added by JustinSainton 12 years ago.
wp_http_ prefix.
25840.2.diff (1.1 KB) - added by leewillis77 12 years ago.
With the previous patch that the function would still block requests if WP_ACCESSIBLE_HOSTS was undefined - without ever invoking the filter. Revised patch attached - this includes the suggested rename as per Justin's patch.
25840.3.diff (2.3 KB) - added by dd32 12 years ago.

Download all attachments as: .zip

Change History (22)

@leewillis77
12 years ago

Proposed patch

#1 @leewillis77
12 years ago

Would that patch allow you to do what you want to (By filtering the list of accessible sites within block_request()) ?

#2 @ocean90
12 years ago

  • Type changed from feature request to enhancement

-1 for an UI option.

A filter like 25840.diff looks good for me.

#3 @dd32
12 years ago

A filter on any/all existing constants sounds like a good idea to me.

#4 @dd32
12 years ago

  • Milestone changed from Awaiting Review to 3.8

Only note, is that we currently have filters prefixed with 'wp_http_' and 'http_', so I'd prefix that filter with the former.

@JustinSainton
12 years ago

wp_http_ prefix.

#5 @JustinSainton
12 years ago

25840.1.diff prefixes the filter as requested.

@leewillis77
12 years ago

With the previous patch that the function would still block requests if WP_ACCESSIBLE_HOSTS was undefined - without ever invoking the filter. Revised patch attached - this includes the suggested rename as per Justin's patch.

#6 @xFireFartx
12 years ago

Hi,
thanks for the patch! I want to discuss the use of a filter here.
Currently the WP_ACCESSIBLE_HOSTS can only be managed by an admin because it is a constant and can not be overwritten by a plugin. With the use of a filter, every plugin can register it's own domains. I think the purpose of the whitelist is that it is managed by an admin and not via plugins.
Example: A plugin sends usage statistics to xxx.yyy.com. Now an admin can manage the whitelist and deny the access to this site. With the use of a filter the plugin can register xxx.yyy.com for the whitelist and the admin can not deny it.

How do you see this whitelisting feature?

#7 follow-up: @leewillis77
12 years ago

With the use of a filter, every plugin can register it's own domains.

Correct - but plugins may also remove domains.

A plugin sends usage statistics to xxx.yyy.com. Now an admin can manage the whitelist and deny the access to this site. With the use of a filter the plugin can register xxx.yyy.com for the whitelist and the admin can not deny it.

The admin can deny it by setting the priority of their own filter higher than that of the plugins.

#8 @xFireFartx
12 years ago

Ok that will do the trick :)

#9 in reply to: ↑ 7 ; follow-up: @Christian Buchhas
12 years ago

Replying to leewillis77:

With the use of a filter, every plugin can register it's own domains.

Correct - but plugins may also remove domains.

A plugin sends usage statistics to xxx.yyy.com. Now an admin can manage the whitelist and deny the access to this site. With the use of a filter the plugin can register xxx.yyy.com for the whitelist and the admin can not deny it.

The admin can deny it by setting the priority of their own filter higher than that of the plugins.

Hi,

that is an interesting argument, but there is a possibility where you can break the security!
When the plugin uses the maximum prio, then there is no more room for the admin to add a higher prio, and the queue of filters will be processed with the order of their names ... ?

The easiest and most secure solution is to set the constants in a plugin, which name begin with zero, because the plugins will be executed with the order of their names!

The constant hasn't be set in wp-config.php, so there is no problem.

Best regards,
Christian

This filter destroys the security feature of the constants

#10 in reply to: ↑ 9 ; follow-up: @rmccue
12 years ago

Replying to Christian Buchhas:

that is an interesting argument, but there is a possibility where you can break the security!
When the plugin uses the maximum prio, then there is no more room for the admin to add a higher prio, and the queue of filters will be processed with the order of their names ... ?

The easiest and most secure solution is to set the constants in a plugin, which name begin with zero, because the plugins will be executed with the order of their names!

That would be true, but there's not really a "maximum" priority as such. Priorities aren't guaranteed to be integers, floats, or even numbers, so there's (almost) always something higher that you can set. e.g. if something uses PHP_INT_MAX, you can use PHP_INT_MAX + 1 and it'll transparently become a larger float. (Interesting thought: what *is* the maximum priority (last sorted value) you can get in PHP? I'd guess INF.)

---

Apart from the intellectual exercise, I'm not sure it really matters. If you enable a plugin, it can already run arbitrary code, so it's hardly a security issue.

Last edited 12 years ago by rmccue (previous) (diff)

#11 in reply to: ↑ 10 @dd32
12 years ago

Replying to rmccue:

Apart from the intellectual exercise, I'm not sure it really matters. If you enable a plugin, it can already run arbitrary code, so it's hardly a security issue.

Yeah, these constants aren't designed as a security measure (you should have a firewall between the web server and the LAN/Web if you need to prevent connections) and are rather more designed to prevent HTTP timeouts to inaccessible hosts.

Looking at 25840.2.diff we'll also need to change the code further down that expects WP_ACCESSIBLE_HOSTS to be defined. It should instead be changed to loop over the filters results, and if * is present, add it to the wildcard list, etc.

@dd32
12 years ago

#12 @dd32
12 years ago

  • Keywords has-patch needs-testing added

25840.3.diff is an untested patch that covers all angles of this constant & filter.

The previous patches filter only once per page load, where as this filters on each request, in addition, the logic has been changed to accomodate that in hopefully a more performance-friendly manner.

#13 @dd32
12 years ago

  • Milestone changed from 3.8 to Future Release

#14 @chriscct7
10 years ago

  • Keywords needs-refresh added

#15 @desrosj
5 years ago

  • Milestone set to Future Release

The patch here still needs a refresh.

This ticket was mentioned in PR #9824 on WordPress/wordpress-develop by @pmbaldha.


5 months ago
#16

  • Keywords needs-refresh removed

@mukesh27 commented on PR #9824:


5 months ago
#17

The PR is not refresh of https://core.trac.wordpress.org/attachment/ticket/25840/25840.3.diff. Some code changes missing in the PR.

#18 @ozgursar
4 weeks ago

  • Keywords needs-testing removed

Test Report

Description

This report validates whether the indicated patch works as expected.

Patch tested: https://github.com/WordPress/wordpress-develop/pull/9824

Environment

  • WordPress: 7.0-alpha-61215-src
  • PHP: 8.2.29
  • Server: nginx/1.29.4
  • Database: mysqli (Server: 8.4.7 / Client: mysqlnd 8.2.29)
  • Browser: Chrome 144.0.0.0
  • OS: macOS
  • Theme: Twenty Twenty-One 2.7
  • MU Plugins: None activated
  • Plugins:
    • Code Snippets 3.9.4
    • Test Reports 1.2.1

Steps to Reproduce

  1. Add the following code via Code Snippets plugin, via your functions.php or install it as a plugin.
/**
 * Plugin Name: Test wp_accessible_hosts Filter & Backwards Compatibility
 * Description: Tests the new wp_accessible_hosts filter, WP_ACCESSIBLE_HOSTS constant backwards compatibility, and makes real API calls
 * Version: 2.0
 * Author: Ozgur Sar
 */

// Add our test hosts via the new filter if it exists
add_filter( 'wp_http_accessible_hosts', function ( $hosts ) {

    if ( ! is_array( $hosts ) ) {
        $hosts = [];
    }

    $hosts[] = 'jsonplaceholder.typicode.com';
    $hosts[] = 'api.github.com';	

    return $hosts;

}, 10, 1 );

add_action( 'admin_notices', 'test_wp_accessible_hosts_comprehensive' );

function test_wp_accessible_hosts_comprehensive() {
    // Check if external requests are blocked
    $external_blocked = defined( 'WP_HTTP_BLOCK_EXTERNAL' ) && WP_HTTP_BLOCK_EXTERNAL;
    
    // Check if the constant is defined
    $constant_defined = defined( 'WP_ACCESSIBLE_HOSTS' );
    $constant_value = $constant_defined ? WP_ACCESSIBLE_HOSTS : 'Not defined';
    
    // Check if the filter exists in the code
    $http_file = ABSPATH . 'wp-includes/class-wp-http.php';
    $filter_in_code = false;
    $filter_lines = array();
    
    if ( file_exists( $http_file ) ) {
        $content = file_get_contents( $http_file );
        $filter_in_code = strpos( $content, 'wp_accessible_hosts' ) !== false;
        
        if ( $filter_in_code ) {
            $lines = explode( "\n", $content );
            foreach ( $lines as $line_num => $line ) {
                if ( strpos( $line, 'wp_accessible_hosts' ) !== false ) {
                    $filter_lines[] = array(
                        'num' => $line_num + 1,
                        'content' => trim( $line )
                    );
                }
            }
        }
    }
    
    // Check if filter has callbacks
    $has_callbacks = has_filter( 'wp_accessible_hosts' );
    
    // Test applying the filter
    $test_hosts = array( 'example.com' );
    $filtered_hosts = apply_filters( 'wp_accessible_hosts', $test_hosts );
    
    // Perform real API tests
    $api_test_results = array();
    
    // Test 1: JSONPlaceholder API (should work with our filter)
    $api_test_results['jsonplaceholder'] = test_api_call( 'https://jsonplaceholder.typicode.com/posts/1' );
    
    // Test 2: GitHub API (should work with our filter)
    $api_test_results['github'] = test_api_call( 'https://api.github.com/zen' );
    
    // Test 3: Random API that's not whitelisted (should fail if blocking is enabled)
    $api_test_results['httpbin'] = test_api_call( 'https://httpbin.org/get' );
    
    ?>
    <div class="notice notice-info" style="padding: 15px;">
        <h2 style="margin-top: 0;">🔍 wp_accessible_hosts Filter & Backwards Compatibility Test</h2>
        
        <!-- Configuration Status -->
        <h3>📋 Current Configuration</h3>
        <table class="widefat" style="max-width: 900px; background: white; margin-bottom: 20px;">
            <thead>
                <tr>
                    <th>Setting</th>
                    <th>Status</th>
                    <th>Details</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td><strong>WP_HTTP_BLOCK_EXTERNAL</strong></td>
                    <td><?php echo $external_blocked ? '🔴 Enabled (Blocking)' : '🟢 Disabled (Allowing All)'; ?></td>
                    <td>
                        <?php echo $external_blocked ? 'External requests are being blocked' : 'All external requests allowed'; ?>
                        <?php if ( ! $external_blocked ) : ?>
                            <br><strong style="color: #996800;">⚠️ NOTE: WP_ACCESSIBLE_HOSTS and wp_accessible_hosts filter have NO EFFECT when blocking is disabled!</strong>
                        <?php endif; ?>
                    </td>
                </tr>
                <tr>
                    <td><strong>WP_ACCESSIBLE_HOSTS constant</strong></td>
                    <td><?php echo $constant_defined ? '✅ Defined' : '❌ Not Defined'; ?></td>
                    <td>
                        <code><?php echo esc_html( $constant_value ); ?></code>
                        <?php if ( $constant_defined && ! $external_blocked ) : ?>
                            <br><span style="color: #996800;">⚠️ This constant is ignored because WP_HTTP_BLOCK_EXTERNAL is false</span>
                        <?php endif; ?>
                    </td>
                </tr>
            </tbody>
        </table>
                
        <!-- Live API Tests -->
        <h3>Live API Request Tests</h3>
        <p style="background: #e7f5fe; padding: 10px; border-left: 4px solid #2271b1; margin-bottom: 10px;">
            <strong>💡 KEY CONCEPT:</strong> These tests make <strong>real wp_remote_get() calls</strong> through WordPress core's HTTP class. 
            If the patch is applied, WordPress will apply the <code>wp_accessible_hosts</code> filter during these requests.
        </p>
        <table class="widefat" style="max-width: 900px; background: white; margin-bottom: 20px;">
            <thead>
                <tr>
                    <th>API</th>
                    <th>URL</th>
                    <th>Result</th>
                    <th>Details</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td><strong>JSONPlaceholder</strong><br><small>(whitelisted via filter)</small></td>
                    <td><code style="font-size: 11px;">jsonplaceholder.typicode.com</code></td>
                    <td><?php echo format_api_result( $api_test_results['jsonplaceholder'] ); ?></td>
                    <td>
                        <?php if ( $api_test_results['jsonplaceholder']['success'] ) : ?>
                            <details>
                                <summary style="cursor: pointer; color: #2271b1;">Show response</summary>
                                <pre style="font-size: 11px; max-height: 150px; overflow-y: auto; background: #f6f7f7; padding: 10px; margin-top: 5px;"><?php echo esc_html( substr( $api_test_results['jsonplaceholder']['body'], 0, 500 ) ); ?></pre>
                            </details>
                        <?php else : ?>
                            <span style="color: #d63638;"><?php echo esc_html( $api_test_results['jsonplaceholder']['message'] ); ?></span>
                        <?php endif; ?>
                    </td>
                </tr>
                <tr>
                    <td><strong>GitHub API</strong><br><small>(whitelisted via filter)</small></td>
                    <td><code style="font-size: 11px;">api.github.com</code></td>
                    <td><?php echo format_api_result( $api_test_results['github'] ); ?></td>
                    <td>
                        <?php if ( $api_test_results['github']['success'] ) : ?>
                            <?php if ( $external_blocked && $filter_in_code ) : ?>
                                <span style="color: #00a32a; font-weight: bold;">✅ FILTER IS WORKING IN CORE!</span><br>
                                <small>This API is ONLY whitelisted via the filter (not in constant), and it succeeded!</small>
                            <?php elseif ( $external_blocked && ! $filter_in_code ) : ?>
                                <span style="color: #996800; font-weight: bold;">⚠️ Unexpected Success</span><br>
                                <small>Blocking is enabled but filter not in core - this shouldn't work!</small>
                            <?php else : ?>
                                <span style="color: #00a32a;">Success (blocking disabled)</span>
                            <?php endif; ?>
                            <details>
                                <summary style="cursor: pointer; color: #2271b1;">Show response</summary>
                                <pre style="font-size: 11px; background: #f6f7f7; padding: 10px; margin-top: 5px;"><?php echo esc_html( $api_test_results['github']['body'] ); ?></pre>
                            </details>
                        <?php else : ?>
                            <?php if ( $external_blocked && ! $filter_in_code ) : ?>
                                <span style="color: #d63638; font-weight: bold;">✅ EXPECTED FAILURE</span><br>
                                <small>Filter not in core, so this API should be blocked. This confirms the patch is NOT applied.</small>
                            <?php else : ?>
                                <span style="color: #d63638;"><?php echo esc_html( $api_test_results['github']['message'] ); ?></span>
                            <?php endif; ?>
                        <?php endif; ?>
                    </td>
                </tr>
                <tr>
                    <td><strong>HTTPBin</strong><br><small>(NOT whitelisted)</small></td>
                    <td><code style="font-size: 11px;">httpbin.org</code></td>
                    <td><?php echo format_api_result( $api_test_results['httpbin'] ); ?></td>
                    <td>
                        <?php if ( $api_test_results['httpbin']['success'] ) : ?>
                            <span style="color: #00a32a;">Request succeeded (blocking not enabled or filter working)</span>
                        <?php else : ?>
                            <span style="color: #996800;">Expected: should be blocked if WP_HTTP_BLOCK_EXTERNAL is true</span>
                        <?php endif; ?>
                    </td>
                </tr>
            </tbody>
        </table>
        
    </div>
    <?php
}

/**
 * Make a test API call
 */
function test_api_call( $url ) {
    $response = wp_remote_get( $url, array(
        'timeout' => 5,
        'sslverify' => true,
    ) );
    
    if ( is_wp_error( $response ) ) {
        return array(
            'success' => false,
            'message' => $response->get_error_message(),
            'body' => ''
        );
    }
    
    $response_code = wp_remote_retrieve_response_code( $response );
    $body = wp_remote_retrieve_body( $response );
    
    return array(
        'success' => $response_code >= 200 && $response_code < 300,
        'message' => 'HTTP ' . $response_code,
        'body' => $body,
        'code' => $response_code
    );
}

/**
 * Format API result for display
 */
function format_api_result( $result ) {
    if ( $result['success'] ) {
        return '<span style="color: #00a32a; font-weight: bold;">✅ Success</span><br><small>HTTP ' . $result['code'] . '</small>';
    } else {
        return '<span style="color: #d63638; font-weight: bold;">❌ Failed</span><br><small>' . esc_html( $result['message'] ) . '</small>';
    }
}
  1. Check WP Dashboard home to view the patch implementation results.

Actual Results

  1. ✅ Issue resolved with patch.
  2. ❌ Without patch applied, allowed hosts via filter are not reachable (e.g. jsonplaceholder.typicode.com and api.github.com in this case. Change them in the plugin file for your test case.)
  3. ✅ When patch applied, hosts allowed via filter are reachable. See screenshots below for example outputs and actual http response from the jsonplaceholder.typicode.com API.

Additional Notes

  • To be able to test the patch, add wp-config.php and add the following lines:
define( 'WP_HTTP_BLOCK_EXTERNAL', true );
define( 'WP_ACCESSIBLE_HOSTS', 'example.com' );

  • Defining only WP_HTTP_BLOCK_EXTERNAL is not enough. Filter works only if WP_ACCESSIBLE_HOSTS is defined and at least set to an empty string.
  • When I use the filter, I received the following PHP warning:

Warning: Undefined variable $w_accessible_hosts in /var/www/src/wp-includes/class-wp-http.php

Supplemental Artifacts

Before:
https://i.imgur.com/ICPpur8.png

After:
https://i.imgur.com/vuIMR9r.png

Example response from jsonplaceholder.typicode.com
https://i.imgur.com/N6Tlrzq.png

Last edited 4 weeks ago by ozgursar (previous) (diff)
Note: See TracTickets for help on using tickets.