WordPress · Report #3801107
Unauthenticated disclosure of private and hidden forum content in bbPress via the WordPress core REST single-item endpoint
How it was found
Description:
bbPress restricts the topics and replies inside private and hidden forums to authorized members. The current development version (2.7.0) registers the forum, topic, and reply post types with show_in_rest => true, which exposes them through the WordPress core REST API. bbPress enforces forum visibility only through a pre_get_posts query filter, and the core REST single-item endpoint reads a post directly by ID without running that filter. As a result, an unauthenticated attacker can read the full content of any topic or reply located in a private or hidden forum by requesting it from the core REST API by ID. bbPress denies that same anonymous request on its front-end, on the REST collection endpoint, and on the forum's own REST endpoint, so the single-item exposure reflects an enforcement path that was never extended to REST, not a decision to publish members-only content.
Topics and replies are stored with post_status = publish regardless of the visibility of the forum that contains them. Topics are created with the public status in src/includes/topics/functions.php:33 ('post_status' => bbp_get_public_status_id()), and replies likewise. Only the forum container carries the restricted status: a private forum has the core private status and a hidden forum has bbPress's custom hidden status (registered with 'private' => true in src/bbpress.php). bbPress keeps the restricted topics and replies out of normal listings by adding their parent forums to an exclusion set inside bbp_pre_get_posts_normalize_forum_visibility() (src/includes/forums/functions.php:2329), which is hooked to pre_get_posts.
As of 2.7.0 the three post types are registered with show_in_rest => true (src/bbpress.php:548, :578, :608), so WordPress automatically registers the default WP_REST_Posts_Controller routes for them at /wp-json/wp/v2/forum, /wp-json/wp/v2/topic, and /wp-json/wp/v2/reply. The released 2.6.x line does not set show_in_rest, so the post types default to being excluded from REST and are not affected. This change was introduced by the commit that added Gutenberg block support to the post types.
The core REST single-item endpoint does not apply bbPress's visibility filter. WP_REST_Posts_Controller::get_item() resolves the post with get_post( $id ), which is a direct primary-key lookup that does not run a WP_Query and therefore never triggers pre_get_posts. The permission check, WP_REST_Posts_Controller::check_read_permission(), returns true for any post whose status is publish without consulting capabilities or the parent forum. Because topics and replies in private and hidden forums are publish, the check passes for an anonymous caller and the controller returns the full content.rendered. bbPress registers no REST permission filter (no rest_{post_type}_item_permissions_check, no register_rest_field permission callback, no rest_{post_type}_query adjustment) that would re-impose forum visibility on this path.
The REST collection endpoint is not affected, which isolates the gap to the single-item path. bbp_pre_get_posts_normalize_forum_visibility() does not bail for REST or non-main queries, so the WP_Query that backs GET /wp-json/wp/v2/topic is filtered and the restricted topics are excluded. The single-item endpoint bypasses that query entirely.
This was confirmed end to end at runtime against WordPress with bbPress 2.7.0 trunk (attached). A hidden forum and a private forum were created with bbPress's own API, each containing a topic with secret content, plus a reply in the hidden forum. As an unauthenticated client:
GET /wp-json/wp/v2/topic/5(topic in the hidden forum) returned HTTP 200 with the full contentSECRET-TOPIC: acquiring Acme Corp for $4.2B, board vote Friday.GET /wp-json/wp/v2/reply/6(reply in the hidden forum) returned HTTP 200 withSECRET-REPLY: wire the deposit to account 12345 before close.GET /wp-json/wp/v2/topic/9(topic in the private forum) returned HTTP 200 withSECRET-PRIVATE: payouts hit wallets Monday 09:00 UTC.
The same anonymous client was denied on every other path: the REST collection GET /wp-json/wp/v2/topic returned an empty array (the restricted topics are excluded), the front-end permalink returned HTTP 404, and the hidden forum's own REST endpoint GET /wp-json/wp/v2/forum/4 returned HTTP 401 rest_forbidden. The denials on those three paths establish that the data is members-only and that only the single-item REST route fails to honor it.
Topic and reply IDs are sequential integers, so the restricted corpus is harvested by iterating IDs. Iterating topic/1 through topic/10 as an unauthenticated client returned content only for the two restricted topics and 404 for the rest, demonstrating an unauthenticated, scalable read of members-only forum content with no knowledge of IDs required beyond enumeration.
The affected version is the 2.7.0 development line (trunk), where the forum post types are newly exposed to REST. The released 2.6.x line does not set show_in_rest and is not affected, so reporting it now prevents the regression from reaching the installed base when 2.7.0 ships.
Steps To Reproduce:
- Install WordPress with bbPress 2.7.0 (trunk) active and pretty permalinks enabled.
- Create a hidden forum (or a private forum) and post a topic and a reply in it. The topic and reply are stored with
post_status = publish; only the forum carries the hidden or private status. - As an unauthenticated client (no cookie, no auth header), request
GET /wp-json/wp/v2/topic/<topic_id>andGET /wp-json/wp/v2/reply/<reply_id>. The response is HTTP 200 and the JSONcontent.renderedfield contains the full topic or reply content. - Confirm the data is otherwise restricted for the same anonymous client:
GET /wp-json/wp/v2/topicreturns an array that excludes the topic, the topic's front-end permalink returns HTTP 404, andGET /wp-json/wp/v2/forum/<forum_id>returns HTTP 401. - Iterate
<topic_id>and<reply_id>over the sequential ID space to harvest the private and hidden forum corpus.
Recommendations
Re-impose forum visibility on the REST path, or do not expose the content to REST at all. Any one of the following resolves it:
- Register a
permission_callback/rest_{post_type}_item_permissions_checkfilter for the forum, topic, and reply post types that callsbbp_current_user_can_see()(or the equivalent ancestor-forum visibility check) on the requested object and denies the read when the current user cannot see the containing forum. This mirrors the visibility logic already enforced on the front-end and onWP_Query. - Alternatively, scope
show_in_restto capability (for example, expose the content in REST only for users who can moderate or edit the relevant post type), so the Gutenberg editing use case is served without exposing unauthenticated reads. - Alternatively, register a custom
rest_controller_classthat filters both collection and single reads through the existing forum-visibility logic.
The fix should cover both the single-item and collection endpoints for all three post types, and should be verified against the controls above (front-end 404, collection exclusion, forum 401) so the REST single-item path matches the visibility enforced everywhere else.
Supporting Material/References:
bbpress_rest_disclosure_poc.txt: captured transcript of the unauthenticated single-item REST requests returning the hidden- and private-forum topic and reply content, the control checks (collection exclusion, front-end 404, forum 401), and the ID-enumeration harvest.reproduce.shanddocker-compose.yml: stand up WordPress + bbPress 2.7.0 trunk, create the hidden and private forums with secret topics/replies, and run the requests and the controls.- Source references (bbPress trunk):
src/bbpress.php:548,578,608(show_in_rest => trueon forum/topic/reply);src/includes/forums/functions.php:2329(bbp_pre_get_posts_normalize_forum_visibility, hooked topre_get_posts);src/includes/topics/functions.php:33(topics created withbbp_get_public_status_id()= publish). bbPress registers no REST permission filter for these post types. WordPress core:WP_REST_Posts_Controller::check_read_permission()returns true forpublishstatus, andget_item()resolves the post viaget_post()without aWP_Query.
AI disclosure:
This report was produced with AI assistance. An automated source-analysis pass identified the candidate by reviewing the bbPress code for read paths that bypass the plugin's forum-visibility enforcement. The finding was then verified end to end against a live WordPress + bbPress 2.7.0 trunk instance, and the code citations, the runtime proof of concept, and this writeup were assembled and checked with AI tooling under human review. Every code reference and the attached transcript were confirmed against the running build.
Impact
An unauthenticated attacker can read the full content of topics and replies posted in private and hidden bbPress forums. That content is restricted to forum members, and bbPress denies it to anonymous users on the front-end, on the REST collection endpoint, and on the forum's own REST endpoint. The disclosed unit is the complete topic and reply content, not a truncated excerpt. Because topic and reply IDs are sequential integers, the attacker harvests the entire restricted forum corpus by ID iteration, with no authentication, account, or user interaction required.
The issue is present in the 2.7.0 development line, which newly exposes these post types to REST, and not in the released 2.6.x line. Reporting it before release prevents the regression from shipping to the bbPress installed base.