Bogo <= 3.9.1 - Missing Authorization to Authenticated (Subscriber+) Sensitive Information Exposure via REST API
Description
The Bogo plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 3.9.1 via the bogo_rest_create_post_translation. This makes it possible for authenticated attackers, with subscriber-level access and above, to extract the raw title, content, excerpt, and password of any private, draft, or password-protected post by triggering its duplication via the translation endpoint and reading the returned title.raw, content.raw, and excerpt.raw fields of the duplicated post. This vulnerability is exploitable against posts written in a non-default locale, as authenticated subscribers can request a translation into the site's default locale to pass the locale-only permission gate. While subscribers can trigger the endpoint, this is only impactful at the Contributor-level as they can actually read the duplicated content.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v3.9.2
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-9013 (Bogo Sensitive Information Exposure) ## 1. Vulnerability Summary The Bogo plugin for WordPress is vulnerable to **Sensitive Information Exposure** in versions up to 3.9.1. The vulnerability exists within the REST API endpoint used to create post translat…
Show full research plan
Exploitation Research Plan: CVE-2026-9013 (Bogo Sensitive Information Exposure)
1. Vulnerability Summary
The Bogo plugin for WordPress is vulnerable to Sensitive Information Exposure in versions up to 3.9.1. The vulnerability exists within the REST API endpoint used to create post translations (bogo_rest_create_post_translation).
The plugin fails to perform adequate authorization checks on the source post being translated. Specifically, the permission_callback for the translation creation route only verifies if the current user has access to the target locale (bogo_access_locale), but does not verify if the user has permission to read or edit the source post. An authenticated attacker with Subscriber-level permissions can trigger a translation of a private, draft, or password-protected post into a locale they have access to (such as the site's default locale), and the REST API response will return the raw content of the newly created (duplicated) post, effectively leaking the original sensitive content.
2. Attack Vector Analysis
- Endpoint:
/wp-json/bogo/v1/posts/(?P<id>\d+)/translations/(?P<locale>[a-z]{2,3}...) - Method:
POST - Vulnerable Function:
bogo_rest_create_post_translationinincludes/rest-api.php. - Authentication Required: Authenticated (Subscriber or higher).
- Preconditions:
- The target post must be of a "localizable" post type (standard posts/pages).
- The target post must be in a non-default locale (to bypass simple locale gates if the subscriber only has access to the default locale).
- The attacker needs a valid
wp_restnonce for the authenticated session.
3. Code Flow
- Entry Point: An authenticated user sends a POST request to
/wp-json/bogo/v1/posts/[ID]/translations/[LOCALE]. - Route Registration (
includes/rest-api.php):- The route calls the
permission_callbackwhich checkscurrent_user_can( 'bogo_access_locale', $locale ). - Subscribers typically have access to the site's default locale.
- The route calls the
- Vulnerable Callback (
bogo_rest_create_post_translation):$post_idis retrieved from the request parameters.$post = get_post( $post_id )retrieves the source post.- The Bug: The function proceeds to duplicate the post using
bogo_duplicate_post( $post, $locale )without checking if the user hasread_postoredit_postcapabilities for the source$post_id.
- Information Leak:
- After duplication, the function constructs a response array:
$response[$locale] = array( // ... 'title' => array( 'rendered' => '', 'raw' => $new_post->post_title ), 'content' => array( 'rendered' => '', 'raw' => $new_post->post_content ), 'excerpt' => array( 'rendered' => '', 'raw' => $new_post->post_excerpt ), ); - Because
$new_postis a duplicate of the private source post,content.rawcontains the full, sensitive text of the original post.
- After duplication, the function constructs a response array:
4. Nonce Acquisition Strategy
The Bogo REST API requires a standard WordPress REST API nonce (wp_rest action).
- Identify Trigger: Bogo enqueues its admin scripts on post editing screens.
- Creation of Environment:
- The agent should log in as a Subscriber.
- Navigate to the WordPress Dashboard (even if limited, Subscribers can access
wp-admin/profile.php).
- Extraction:
- In the browser context, WordPress typically exposes the REST nonce via the
wpApiSettingsobject. - Action:
browser_eval("window.wpApiSettings?.nonce")
- In the browser context, WordPress typically exposes the REST nonce via the
- Alternative: If
wpApiSettingsis not available, the nonce is often found in the_wpnonceattribute of the localized script for Bogo (though the JS source provided suggests it usesapiFetch, which relies on the standardwp_restnonce).
5. Exploitation Strategy
Step 1: Authentication
Authenticate the session as a Subscriber.
Step 2: Identification
Identify a sensitive post ID (e.g., a "Private" post with ID 123 currently in a locale like fr_FR). Identify the site's default locale (e.g., en_US).
Step 3: Trigger Duplication via REST API
Using the http_request tool, send the following request:
- Method:
POST - URL:
http://[TARGET]/wp-json/bogo/v1/posts/123/translations/en_US - Headers:
Content-Type: application/jsonX-WP-Nonce: [EXTRACTED_NONCE]Cookie: [SUBSCRIBER_COOKIES]
- Body:
{}(Parameters are in the URL path).
Step 4: Parse Response
The server will respond with a 200 OK and a JSON object. Locate the en_US key (or whichever locale was requested) and extract:
title.rawcontent.raw
6. Test Data Setup
- Administrator Action:
- Install and activate Bogo.
- Install a secondary language pack (e.g., French).
- Create a "Private" post in French:
wp post create --post_title='Secret French Post' --post_content='This is sensitive internal data.' --post_status='private' --post_author=1 # Assign French locale (Bogo stores this as a term or meta, verify with bogo_set_post_locale) wp eval "bogo_set_post_locale( [POST_ID], 'fr_FR' );"
- Attacker Account:
- Create a Subscriber user:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password123
- Create a Subscriber user:
7. Expected Results
- The REST API call should return a JSON object representing the newly created translation.
- The
content.rawfield in the JSON response must match the content of the private post created by the administrator ('This is sensitive internal data.'). - The attacker should receive this data despite not having permission to view private posts.
8. Verification Steps
- Post-Exploit CLI Check:
- Use
wp post list --post_status=anyto verify that a new post was actually created in the target locale. - Compare the ID returned in the REST response with the ID of the new post in the database.
- Use
- Compare Content:
wp post get [ORIGINAL_ID] --field=post_content- Compare with the data captured in the
content.rawfield of the exploit response.
9. Alternative Approaches
- Targeting Drafts: If no private posts are available, target
draftstatus posts. - Targeting Password-Protected: Target posts where
post_passwordis set. Thebogo_duplicate_postfunction usually copies the content regardless of the password protection status. - Locale Hunting: If
en_USis forbidden, usewp-json/bogo/v1/languagesto list all available locales and iterate through them until one passes thebogo_access_localecheck.
Summary
The Bogo plugin for WordPress is vulnerable to Sensitive Information Exposure via its REST API due to missing authorization checks on the source post during translation creation. Authenticated users with Subscriber-level access can duplicate private, draft, or password-protected posts into a locale they have access to and receive the raw content of those posts in the API response.
Vulnerable Code
// includes/rest-api.php line 40 register_rest_route( 'bogo/v1', '/posts/(?P<id>\d+)/translations/(?P<locale>' . $locale_pattern . ')', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'bogo_rest_create_post_translation', 'permission_callback' => static function( WP_REST_Request $request ) { $locale = $request->get_param( 'locale' ); if ( current_user_can( 'bogo_access_locale', $locale ) ) { return true; } else { return new WP_Error( 'bogo_locale_forbidden', __( 'You are not allowed to access posts in the requested locale.', 'bogo' ), array( 'status' => 403 ) ); } }, ) ); --- // includes/rest-api.php line 186 function bogo_rest_create_post_translation( WP_REST_Request $request ) { $post_id = $request->get_param( 'id' ); $post = get_post( $post_id ); // ... (missing check to see if user can read $post) ... $new_post_id = bogo_duplicate_post( $post, $locale ); if ( ! $new_post_id ) { return new WP_Error( 'bogo_post_duplication_failed', __( 'Failed to duplicate a post.', 'bogo' ), array( 'status' => 500 ) ); } $new_post = get_post( $new_post_id ); $response = array(); $response[$locale] = array( 'lang' => array( 'tag' => bogo_language_tag( $locale ) ), 'id' => $new_post->ID, 'link' => get_permalink( $new_post->ID ), 'edit_link' => get_edit_post_link( $new_post->ID, 'raw' ), 'slug' => $new_post->post_name, 'type' => $new_post->post_type, 'date' => mysql_to_rfc3339( $new_post->post_date ), 'date_gmt' => mysql_to_rfc3339( $new_post->post_date_gmt ), 'modified' => mysql_to_rfc3339( $new_post->post_modified ), 'modified_gmt' => mysql_to_rfc3339( $new_post->post_modified_gmt ), 'guid' => array( 'rendered' => '', 'raw' => $new_post->guid ), 'title' => array( 'rendered' => '', 'raw' => $new_post->post_title ), 'content' => array( 'rendered' => '', 'raw' => $new_post->post_content ), 'excerpt' => array( 'rendered' => '', 'raw' => $new_post->post_excerpt ), );
Security Fix
@@ -31,14 +31,28 @@ 'permission_callback' => static function( WP_REST_Request $request ) { $locale = $request->get_param( 'locale' ); - if ( current_user_can( 'bogo_access_locale', $locale ) ) { - return true; - } else { + if ( ! current_user_can( 'bogo_access_locale', $locale ) ) { return new WP_Error( 'bogo_locale_forbidden', __( 'You are not allowed to access posts in the requested locale.', 'bogo' ), - array( 'status' => 403 ) + array( 'status' => rest_authorization_required_code() ) ); } + + $post_id = $request->get_param( 'id' ); + + if ( $post = get_post( $post_id ) ) { + if ( + ! $post_type_object = get_post_type_object( $post->post_type ) or + ! current_user_can( $post_type_object->cap->create_posts ) + ) { + return new WP_Error( 'bogo_post_cannot_create', + __( 'You are not allowed to create posts as this user.', 'bogo' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + } + + return true; }, ) ); @@ -222,10 +249,10 @@ 'date_gmt' => mysql_to_rfc3339( $new_post->post_date_gmt ), 'modified' => mysql_to_rfc3339( $new_post->post_modified ), 'modified_gmt' => mysql_to_rfc3339( $new_post->post_modified_gmt ), - 'guid' => array( 'rendered' => '', 'raw' => $new_post->guid ), - 'title' => array( 'rendered' => '', 'raw' => $new_post->post_title ), - 'content' => array( 'rendered' => '', 'raw' => $new_post->post_content ), - 'excerpt' => array( 'rendered' => '', 'raw' => $new_post->post_excerpt ), + 'guid' => array( 'rendered' => '', 'raw' => '' ), + 'title' => array( 'rendered' => '', 'raw' => '' ), + 'content' => array( 'rendered' => '', 'raw' => '' ), + 'excerpt' => array( 'rendered' => '', 'raw' => '' ), ); $lang = bogo_get_language( $locale ); @@ -235,16 +262,28 @@ if ( ! empty( $new_post->guid ) ) { $response[$locale]['guid']['rendered'] = apply_filters( 'get_the_guid', $new_post->guid ); + + if ( current_user_can( $edit_post_cap, $new_post->ID ) ) { + $response[$locale]['guid']['raw'] = $new_post->guid; + } } if ( ! empty( $new_post->post_title ) ) { $response[$locale]['title']['rendered'] = get_the_title( $new_post->ID ); + + if ( current_user_can( $edit_post_cap, $new_post->ID ) ) { + $response[$locale]['title']['raw'] = $new_post->post_title; + } } if ( ! empty( $new_post->post_content ) ) { $response[$locale]['content']['rendered'] = apply_filters( 'the_content', $new_post->post_content ); + + if ( current_user_can( $edit_post_cap, $new_post->ID ) ) { + $response[$locale]['content']['raw'] = $new_post->post_content; + } } if ( ! empty( $new_post->post_excerpt ) ) { @@ -252,6 +291,10 @@ 'the_excerpt', apply_filters( 'get_the_excerpt', $new_post->post_excerpt ) ); + + if ( current_user_can( $edit_post_cap, $new_post->ID ) ) { + $response[$locale]['excerpt']['raw'] = $new_post->post_excerpt; + } }
Exploit Outline
The exploit involves an authenticated attacker with Subscriber-level permissions sending a POST request to the `/wp-json/bogo/v1/posts/[ID]/translations/[LOCALE]` endpoint. By targeting a private or draft post ID and requesting a translation into a locale the subscriber has permission to access (such as the site's default locale), the plugin duplicates the post and returns the full `raw` title, content, and excerpt in the JSON response. This allows the attacker to bypass standard WordPress post visibility controls.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.