CVE-2026-4066

Smart Custom Fields <= 5.0.6 - Missing Authorization to Authenticated (Contributor+) Sensitive Information Exposure via Relational Post Search

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
5.0.7
Patched in
1d
Time to patch

Description

The Smart Custom Fields plugin for WordPress is vulnerable to unauthorized access of data due to a missing capability check on the relational_posts_search() function in all versions up to, and including, 5.0.6. This makes it possible for authenticated attackers, with Contributor-level access and above, to read private and draft post content from other authors via the smart-cf-relational-posts-search AJAX action. The function queries posts with post_status=any and returns full WP_Post objects including post_content, but only checks the generic edit_posts capability instead of verifying whether the requesting user has permission to read each individual post.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Low
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=5.0.6
PublishedMarch 23, 2026
Last updatedMarch 23, 2026
Affected pluginsmart-custom-fields

What Changed in the Fix

Changes introduced in v5.0.7

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Vulnerability Research Plan: CVE-2026-4066 Missing Authorization in Smart Custom Fields ## 1. Vulnerability Summary The **Smart Custom Fields** plugin (<= 5.0.6) is vulnerable to sensitive information exposure due to missing granular authorization checks in the `relational_posts_search()` functio…

Show full research plan

Vulnerability Research Plan: CVE-2026-4066 Missing Authorization in Smart Custom Fields

1. Vulnerability Summary

The Smart Custom Fields plugin (<= 5.0.6) is vulnerable to sensitive information exposure due to missing granular authorization checks in the relational_posts_search() function. While the function performs a generic capability check (edit_posts), it fails to verify if the requesting user has the authority to view specific draft or private posts from other authors. By setting post_status => 'any', the function returns full WP_Post objects (including post_content) to any authenticated user with edit_posts capability (Contributor level and above), allowing them to read content they should not have access to.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: smart-cf-relational-posts-search (registered via wp_ajax_smart-cf-relational-posts-search in Smart_Custom_Fields_Field_Related_Posts::init())
  • Vulnerable Parameter: post_types (used to define the scope of the search).
  • Authentication: Authenticated, Contributor-level access (PR:L). Contributors have the edit_posts capability for the default post type.
  • Preconditions:
    1. A sensitive post (Status: draft or private) must exist, authored by a different user (e.g., an Administrator).
    2. The attacker must obtain a valid AJAX nonce.

3. Code Flow

  1. Entry Point: The relational_posts_search() function in classes/fields/class.field-related-posts.php is triggered via the smart-cf-relational-posts-search AJAX action.
  2. Nonce Verification: Calls check_ajax_referer( SCF_Config::NAME . '-relation-post-types', 'nonce' ).
  3. Authorization Check (Flawed):
    $post_type_object = get_post_type_object( $_post_type );
    if ( current_user_can( $post_type_object->cap->edit_posts ) ) {
        $retrievable_post_types[] = $_post_type;
    }
    
    Note: A Contributor passes this check for 'post' because they have the edit_posts capability.
  4. Data Retrieval:
    $args = array(
        'post_type'      => $retrievable_post_types,
        'post_status'    => 'any', // <--- Vulnerable: Includes all statuses
        // ...
    );
    $_posts = get_posts( $args );
    
  5. Information Leak: The entire $_posts array (containing full WP_Post objects) is encoded and returned:
    echo wp_json_encode( $_posts );
    

4. Nonce Acquisition Strategy

The nonce is localized in the WordPress admin area for post editing screens.

  1. Target Variable: smart_cf_relation_post_types.nonce
  2. Location: Enqueued via wp_localize_script with the handle smart-cf-editor-relation-post-types.
  3. Acquisition Steps:
    • Log in as the Contributor user.
    • Navigate to the "Add New Post" page: /wp-admin/post-new.php.
    • Use browser_eval to extract the nonce:
      window.smart_cf_relation_post_types?.nonce
      

5. Exploitation Strategy

  1. Pre-requisite: Create a "Secret Admin Draft" containing sensitive info.
  2. Step 1: Obtain Nonce: Authenticate as a Contributor and extract the nonce from the post-new.php page.
  3. Step 2: Trigger Information Leak: Send an authenticated POST request to admin-ajax.php.

HTTP Request Payload

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    action=smart-cf-relational-posts-search&post_types=post&nonce=[EXTRACTED_NONCE]&s=[SEARCH_TERM_OR_EMPTY]
    

6. Test Data Setup

  1. Administrator (User A):
    • Create a post with post_status='draft' or post_status='private'.
    • Title: Confidential Project Alpha.
    • Content: The password to the vault is 123456.
  2. Contributor (User B):
    • Create a standard Contributor user.
    • Credentials: contributor / contributor-password.
  3. Plugin Setup:
    • Ensure "Smart Custom Fields" is activated.
    • No specific custom field configuration is strictly necessary because the AJAX handler is registered globally upon plugin load.

7. Expected Results

  • The AJAX response will be a JSON array of post objects.
  • One of the objects will have the title Confidential Project Alpha.
  • The post_content field of that object will contain The password to the vault is 123456.
  • This confirms that a Contributor can read drafts/private posts belonging to an Administrator.

8. Verification Steps

  1. Analyze JSON: Inspect the response from the http_request tool.
    # Conceptually:
    cat response.json | jq '.[] | select(.post_title == "Confidential Project Alpha")'
    
  2. WP-CLI Check: Confirm the post status and author via CLI to prove it should have been hidden:
    wp post list --post_type=post --post_status=draft --fields=ID,post_title,post_author --format=csv
    
    Compare the post_author ID with the Contributor's ID to confirm they do not own the post.

9. Alternative Approaches

  • Brute Forcing Post Types: If post is restricted by other means, try leaking content from page, attachment (metadata), or other custom post types registered on the site.
  • Search Filtering: Use the s parameter to search for specific keywords (e.g., "password", "key", "internal") within hidden drafts across the entire site.
  • Pagination Crawl: If there are many posts, use the click_count parameter to iterate through all posts:
    • click_count=1, click_count=2, etc. (Multiplied by the posts_per_page option, usually 10).
Research Findings
Static analysis — not yet PoC-verified

Summary

The Smart Custom Fields plugin for WordPress is vulnerable to unauthorized information exposure via its relational post search AJAX action. Authenticated attackers with Contributor-level permissions can exploit a missing granular capability check to retrieve full post objects, including sensitive content from private and draft posts authored by other users.

Vulnerable Code

// classes/fields/class.field-related-posts.php line 83
	public function relational_posts_search() {
		check_ajax_referer( SCF_Config::NAME . '-relation-post-types', 'nonce' );

		$_posts = array();

		$post_types = filter_input( INPUT_POST, 'post_types' );
		if ( $post_types ) {
			$post_type              = explode( ',', $post_types );
			$retrievable_post_types = array();

			foreach ( $post_type as $_post_type ) {
				$post_type_object = get_post_type_object( $_post_type );

				if ( current_user_can( $post_type_object->cap->edit_posts ) ) {
					$retrievable_post_types[] = $_post_type;
				}
			}

			if ( $retrievable_post_types ) {
				$args = array(
					'post_type'      => $retrievable_post_types,
					'order'          => 'ASC',
					'orderby'        => 'ID',
					'posts_per_page' => -1,
					'post_status'    => 'any',
				);

---

// classes/fields/class.field-related-posts.php line 140
				$_posts = get_posts( $args );
			}
		}

		header( 'Content-Type: application/json; charset=utf-8' );
		echo wp_json_encode( $_posts );
		die();
	}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/classes/fields/class.field-file.php /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/classes/fields/class.field-file.php
--- /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/classes/fields/class.field-file.php	2025-05-29 07:49:14.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/classes/fields/class.field-file.php	2026-03-18 01:06:48.000000000 +0000
@@ -83,7 +83,7 @@
 					'<a href="%s" target="_blank"><img src="%s" alt="%s" />%s</a>%s',
 					wp_get_attachment_url( $value ),
 					esc_url( $image_src ),
-					$image_alt,
+					esc_attr( $image_alt ),
 					esc_attr( $filename ),
 					$btn_remove
 				);
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/classes/fields/class.field-image.php /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/classes/fields/class.field-image.php
--- /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/classes/fields/class.field-image.php	2025-05-29 07:49:14.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/classes/fields/class.field-image.php	2026-03-18 01:06:48.000000000 +0000
@@ -82,7 +82,7 @@
 				$image      = sprintf(
 					'<img src="%s" alt="%s" />%s',
 					esc_url( $image_src ),
-					$image_alt,
+					esc_attr( $image_alt ),
 					$btn_remove
 				);
 				$hide_class = '';
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/classes/fields/class.field-related-posts.php /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/classes/fields/class.field-related-posts.php
--- /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/classes/fields/class.field-related-posts.php	2025-12-11 05:04:46.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/classes/fields/class.field-related-posts.php	2026-03-18 01:06:48.000000000 +0000
@@ -83,15 +83,7 @@
 		$post_types = filter_input( INPUT_POST, 'post_types' );
 		if ( $post_types ) {
 			$post_type              = explode( ',', $post_types );
-			$retrievable_post_types = array();
-
-			foreach ( $post_type as $_post_type ) {
-				$post_type_object = get_post_type_object( $_post_type );
-
-				if ( current_user_can( $post_type_object->cap->edit_posts ) ) {
-					$retrievable_post_types[] = $_post_type;
-				}
-			}
+			$retrievable_post_types = $this->get_retrievable_post_types( $post_type );
 
 			if ( $retrievable_post_types ) {
 				$args = array(
@@ -137,7 +129,8 @@
 				*/
 				$args = apply_filters( SCF_Config::PREFIX . 'custom_related_posts_args_ajax_call', $args, $field_name, $post_type );
 
-				$_posts = get_posts( $args );
+				$_posts = $this->filter_readable_posts_for_current_user( get_posts( $args ) );
+				$_posts = $this->prepare_posts_for_response( $_posts );
 			}
 		}
 
@@ -174,15 +167,7 @@
 		$posts_per_page = get_option( 'posts_per_page' );
 
 		if ( $post_type ) {
-			$retrievable_post_types = array();
-
-			foreach ( $post_type as $_post_type ) {
-				$post_type_object = get_post_type_object( $_post_type );
-
-				if ( current_user_can( $post_type_object->cap->edit_posts ) ) {
-					$retrievable_post_types[] = $_post_type;
-				}
-			}
+			$retrievable_post_types = $this->get_retrievable_post_types( $post_type );
 
 			if ( $retrievable_post_types ) {
 				if ( ! preg_match( '/^\d+$/', $limit ) ) {
@@ -208,7 +193,7 @@
 				$args = apply_filters( SCF_Config::PREFIX . 'custom_related_posts_args_first_load', $args, $name, $post_type );
 
 				// Get posts to show in the first load.
-				$choices_posts = get_posts( $args );
+				$choices_posts = $this->filter_readable_posts_for_current_user( get_posts( $args ) );
 			}
 		}
 
@@ -292,6 +277,66 @@
 		);
 	}
 
+	/**
+	 * Returns post types that the current user can edit.
+	 *
+	 * @param array $post_types Post type slugs.
+	 * @return array
+	 */
+	protected function get_retrievable_post_types( $post_types ) {
+		$retrievable_post_types = array();
+
+		foreach ( $post_types as $_post_type ) {
+			$post_type_object = get_post_type_object( $_post_type );
+
+			if ( ! $post_type_object ) {
+				continue;
+			}
+
+			if ( current_user_can( $post_type_object->cap->edit_posts ) ) {
+				$retrievable_post_types[] = $_post_type;
+			}
+		}
+
+		return $retrievable_post_types;
+	}
+
+	/**
+	 * Returns only posts readable by the current user.
+	 *
+	 * @param array $posts Posts.
+	 * @return array
+	 */
+	protected function filter_readable_posts_for_current_user( $posts ) {
+		$posts = array_filter(
+			$posts,
+			function ( $post ) {
+				return current_user_can( 'read_post', $post->ID );
+			}
+		);
+
+		return array_values( $posts );
+	}
+
+	/**
+	 * Returns only fields needed by the related posts UI.
+	 *
+	 * @param array $posts Posts.
+	 * @return array
+	 */
+	protected function prepare_posts_for_response( $posts ) {
+		return array_map(
+			function ( $post ) {
+				return (object) array(
+					'ID'          => $post->ID,
+					'post_title'  => get_the_title( $post->ID ),
+					'post_status' => $post->post_status,
+				);
+			},
+			$posts
+		);
+	}
+
 	/**
 	 * Displaying the option fields in custom field settings page.
 	 *
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/readme.txt /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/readme.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/readme.txt	2025-12-11 05:04:46.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/readme.txt	2026-03-18 01:06:48.000000000 +0000
@@ -5,7 +5,7 @@
 Requires at least: 6.4
 Requires PHP: 7.4
 Tested up to: 6.8
-Stable tag: 5.0.6
+Stable tag: 5.0.7
 License: GPLv2 or later
 License URI: http://www.gnu.org/licenses/gpl-2.0.html
 
@@ -134,6 +134,9 @@
 
 == Changelog ==
 
+= 5.0.7 =
+* Vulnerability fixes
+
 = 5.0.6 =
 * Fixed a bug that caused a fatal error if post-type was not specified in related posts. [inc2734/smart-custom-fields#110](https://github.com/inc2734/smart-custom-fields/issues/110)
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/smart-custom-fields.php /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/smart-custom-fields.php
--- /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.6/smart-custom-fields.php	2025-12-11 05:04:46.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/smart-custom-fields/5.0.7/smart-custom-fields.php	2026-03-18 01:06:48.000000000 +0000
@@ -3,7 +3,7 @@
  * Plugin name: Smart Custom Fields
  * Plugin URI: https://github.com/inc2734/smart-custom-fields/
  * Description: Smart Custom Fields is a simple plugin that management custom fields.
- * Version: 5.0.6
+ * Version: 5.0.7
  * Author: inc2734
  * Author URI: https://2inc.org
  * Text Domain: smart-custom-fields

Exploit Outline

1. Authenticate as a Contributor-level user. 2. Access any post editor screen in the WordPress admin to extract the 'smart_cf_relation_post_types.nonce' from the localized script 'smart_cf_relation_post_types'. 3. Construct a POST request to `/wp-admin/admin-ajax.php` with the following body parameters: - action: 'smart-cf-relational-posts-search' - nonce: [the extracted nonce] - post_types: 'post' (or any other post type) - s: [optional search term] 4. Send the request. Because the server-side function only checks the generic 'edit_posts' capability and queries with 'post_status=any', the response will contain a JSON array of full WP_Post objects, including those with 'draft' or 'private' status that the current user normally cannot access. The 'post_content' field will contain the sensitive information.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.