If you are testing the security of WordPress websites, you will likely have to look at the REST endpoints. By default, users can be listed with the route “/wp-json/wp/v2/users”. On the latest WordPress version, out of the box, you will get the username and the hashed email. Experienced WordPress administrators and users are aware of the potential disclosure. Therefore, we can see various tutorials online on how to hide this information. The recommended ways are either to disable the REST API completely,
install a security plugin which disables the specific route or block specific request paths.
After evaluating hundreds of websites, we can say that rare are the sites that have totally blocked the feature.
1. HTTP parameter “rest_route”
The first bypass we are presenting is abusing an alternative path to reach the same endpoint. While Worpdress is configured – by default – to support URL rewriting to have search engine and human friendly URLs like https://website.com/2020/12/breaking-news
instead of https://website.com/?p=2678
, behind the scene, every request sent to /wp-json/ is entering the index page with the parameter “rest_route” set to /wp/v2/users.
https://****.com/blog/wp-json/wp/v2/users | BLOCKED |
https://****.com/blog/?rest_route=/wp/v2/users | OK |
2. WordPress.com API
https://blog.*******.com/wp-json/wp/v2/users | BLOCKED |
https://public-api.wordpress.com/rest/v1.1/sites/blog.*******.com/posts | OK |
3. One by one
add_filter( 'rest_endpoints', function( $endpoints ){
if ( isset( $endpoints['/wp/v2/users'] ) ) {
unset( $endpoints['/wp/v2/users'] );
} return $endpoints;
});
In the table below, we can see that one host was refusing to serve the complete list of users. However, we realize that targeting a specific user was not being blocked.
https://www.*****.org/wp-json/wp/v2/users | BLOCKED |
https://www.*****.org/wp-json/wp/v2/users/1 | OK |
4. Case sensitivity
foreach ( $routes as $route => $handlers ) {
$match = preg_match( '@^' . $route . '$@i', $path, $matches );
if ( ! $match ) {
continue;
}
$args = array();
Source: class-wp-rest-server.php
RewriteCond %{QUERY_STRING} bwp/v2/usersb [NC]
RewriteRule ^ - [F]
RewriteCond %{QUERY_STRING} bwp/v2/usersb
https://blog.*****.com/section/news?rest_route=/wp/v2/users | BLOCKED |
https://blog.*****.com/section/news?rest_route=/wp/v2/usErs | OK |
5. Search
On few occasions we encounter APIs that are not explicitly blocked but the /wp/v2/users endpoint is not returning the avatar_urls properties. This is the effect of a third-party security plugin or disabling manually the avatars (Settings > Discussion > Avatars).
Setting that will hide avatars both in web pages and REST response |
We did find a workaround for those as well. The endpoint supports the parameter “search”. Its value is match against all user’s fields including the email address. With simple automation it is possible to discover each email address. The user information associated to an email matched will be returned in the JSON response. From experience, we can estimate that between 200 and 400 requests will be required to reveal one email address.
https://api.*****.com/wp-json/wp/v2/users | BLOCKED |
https://api.*****.com/wp-json/wp/v2/[email protected] https://api.*****.com/wp-json/wp/v2/[email protected] https://api.*****.com/wp-json/wp/v2/[email protected] https://api.*****.com/wp-json/wp/v2/[email protected] https://api.*****.com/wp-json/wp/v2/[email protected] |
OK |
6. Yoast SEO
<script type='application/ld+json' class='yoast-schema-graph yoast-schema-graph--main'>{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https:// ***** /#website","url":"https://www.******.com/","name":"*****",
[...]
},
{
"@type":["Person"],
"@id":"https://www.****.com/#/schema/person/7367999b66**********",
"name":"Fred",
"image":{
"@type":"ImageObject",
"@id":"https://www.******.com/#authorlogo",
"url":"https://secure.gravatar.com/avatar/de04459893a29***********?s=96&d=mm&r=g",
"caption":"****"
},
"sameAs":[]
}]
</script>
Conclusion
This blog was originally posted on GoSecure blog.