Smart Custom Fields <= 5.0.6 - Missing Authorization to Authenticated (Contributor+) Sensitive Information Exposure via Relational Post Search
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:NTechnical Details
<=5.0.6What Changed in the Fix
Changes introduced in v5.0.7
Source Code
WordPress.org SVN# 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 viawp_ajax_smart-cf-relational-posts-searchinSmart_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 theedit_postscapability for the defaultposttype. - Preconditions:
- A sensitive post (Status:
draftorprivate) must exist, authored by a different user (e.g., an Administrator). - The attacker must obtain a valid AJAX nonce.
- A sensitive post (Status:
3. Code Flow
- Entry Point: The
relational_posts_search()function inclasses/fields/class.field-related-posts.phpis triggered via thesmart-cf-relational-posts-searchAJAX action. - Nonce Verification: Calls
check_ajax_referer( SCF_Config::NAME . '-relation-post-types', 'nonce' ). - Authorization Check (Flawed):
Note: A Contributor passes this check for 'post' because they have the$post_type_object = get_post_type_object( $_post_type ); if ( current_user_can( $post_type_object->cap->edit_posts ) ) { $retrievable_post_types[] = $_post_type; }edit_postscapability. - Data Retrieval:
$args = array( 'post_type' => $retrievable_post_types, 'post_status' => 'any', // <--- Vulnerable: Includes all statuses // ... ); $_posts = get_posts( $args ); - Information Leak: The entire
$_postsarray (containing fullWP_Postobjects) 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.
- Target Variable:
smart_cf_relation_post_types.nonce - Location: Enqueued via
wp_localize_scriptwith the handlesmart-cf-editor-relation-post-types. - Acquisition Steps:
- Log in as the Contributor user.
- Navigate to the "Add New Post" page:
/wp-admin/post-new.php. - Use
browser_evalto extract the nonce:window.smart_cf_relation_post_types?.nonce
5. Exploitation Strategy
- Pre-requisite: Create a "Secret Admin Draft" containing sensitive info.
- Step 1: Obtain Nonce: Authenticate as a Contributor and extract the nonce from the
post-new.phppage. - 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
- Administrator (User A):
- Create a post with
post_status='draft'orpost_status='private'. - Title:
Confidential Project Alpha. - Content:
The password to the vault is 123456.
- Create a post with
- Contributor (User B):
- Create a standard Contributor user.
- Credentials:
contributor / contributor-password.
- 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_contentfield of that object will containThe password to the vault is 123456. - This confirms that a Contributor can read drafts/private posts belonging to an Administrator.
8. Verification Steps
- Analyze JSON: Inspect the response from the
http_requesttool.# Conceptually: cat response.json | jq '.[] | select(.post_title == "Confidential Project Alpha")' - WP-CLI Check: Confirm the post status and author via CLI to prove it should have been hidden:
Compare thewp post list --post_type=post --post_status=draft --fields=ID,post_title,post_author --format=csvpost_authorID with the Contributor's ID to confirm they do not own the post.
9. Alternative Approaches
- Brute Forcing Post Types: If
postis restricted by other means, try leaking content frompage,attachment(metadata), or other custom post types registered on the site. - Search Filtering: Use the
sparameter 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_countparameter to iterate through all posts:click_count=1,click_count=2, etc. (Multiplied by theposts_per_pageoption, usually 10).
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
@@ -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 ); @@ -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 = ''; @@ -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. * @@ -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) @@ -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.