MW WP Form <= 5.1.2 - Insecure Direct Object Reference to Unauthenticated Sensitive Information Disclosure via 'post_id' Query Parameter
Description
The MW WP Form plugin for WordPress is vulnerable to Information Exposure in all versions up to, and including, 5.1.2 via the _get_post_property_from_querystring() function due to insufficient restrictions on which posts can be included. This makes it possible for unauthenticated attackers to extract data from password protected, private, or draft posts that they should not have access to.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v5.1.3
Source Code
WordPress.org SVNThis research plan outlines the technical analysis and proof-of-concept (PoC) steps for CVE-2026-6206, an Insecure Direct Object Reference (IDOR) vulnerability in the MW WP Form plugin. --- ### 1. Vulnerability Summary The MW WP Form plugin suffers from an Information Exposure vulnerability due to…
Show full research plan
This research plan outlines the technical analysis and proof-of-concept (PoC) steps for CVE-2026-6206, an Insecure Direct Object Reference (IDOR) vulnerability in the MW WP Form plugin.
1. Vulnerability Summary
The MW WP Form plugin suffers from an Information Exposure vulnerability due to an insecurely implemented feature that allows pre-filling form data from URL parameters. The function _get_post_property_from_querystring() in classes/models/class.parser.php retrieves a post object based on a user-supplied post_id query parameter and extracts its properties or meta data without verifying the post's status (e.g., draft, private, or password-protected) or the current user's authorization to view that post.
2. Attack Vector Analysis
- Vulnerable Endpoint: Any public-facing page or post where an MW WP Form shortcode is embedded, provided the form has the "querystring" setting enabled.
- Action: A
GETrequest to the page containing the form. - Vulnerable Parameter:
post_id(Query String). - Required Authentication: None (Unauthenticated).
- Preconditions:
- The form must have the "URL parameter" (internal key:
querystring) setting enabled. - The form content must contain placeholders using the curly brace syntax, such as
{post_title},{post_content}, or{any_meta_key}.
- The form must have the "URL parameter" (internal key:
3. Code Flow
- Entry Point: A user visits a page containing the shortcode
[mwform_formkey key="..."]. - Controller: The shortcode triggers the form rendering process. The
MW_WP_Form_Parseris instantiated to handle placeholder replacement. - Parsing: The method
MW_WP_Form_Parser::replace_for_page()is called to process the form's HTML content. - Property Replacement:
replace_for_page()calls_replace_post_property(). - Setting Check:
_replace_post_property()checks if the settingquerystringis truthy:if ( $this->Setting->get( 'querystring' ) ) { $callback = array( $this, '_get_post_property_from_querystring' ); } - IDOR Sink:
_get_post_property_from_querystring()is executed. It retrieves the post ID from$_GET['post_id']and callsget_post():$post = get_post( $_GET['post_id'] ); - Disclosure: The parser then calls
_get_post_property($post, $meta_key), which returns the requested property (likepost_content) or callsget_post_meta()for the object. No checks forpost_statusor visibility are performed.
4. Nonce Acquisition Strategy
This vulnerability occurs during the rendering of the form (Read operation), not during form submission (Write operation).
- The
replace_for_page()method is called when the WordPress shortcode is expanded into HTML for display. - No nonce is required to trigger this disclosure, as the
post_idis processed via a standardGETrequest to a public page.
5. Test Data Setup
To reproduce the vulnerability, the environment must be configured as follows:
Create Sensitive Content:
wp post create --post_type=post --post_title="Secret Draft" --post_content="FLAG{IDOR_INFO_DISCLOSURE}" --post_status=draft # Note the resulting Post ID (e.g., 10)Create a Vulnerable Form:
Create anmw-wp-formpost. Usewp evalto set the necessary meta data if not using the UI.# Create the form FORM_ID=$(wp post create --post_type=mw-wp-form --post_title="Exploit Form" --post_status=publish --porcelain) # Enable the 'querystring' setting (URL parameter feature) # The setting is typically stored in the 'mw-wp-form' post meta under a specific key. # In versions <= 5.1.2, settings are serialized in the post meta. wp post bash-eval $FORM_ID 'update_post_meta($post_id, "querystring", "1");' # Set the form content to include a leak tag wp post update $FORM_ID --post_content='Title: {post_title} | Content: {post_content}'Embed the Form:
wp post create --post_type=page --post_title="Public Form Page" --post_status=publish --post_content="[mwform_formkey key='mwform_$FORM_ID']" # Note the resulting Page URL (e.g., /public-form-page/)
6. Exploitation Strategy
Perform an unauthenticated GET request to the page hosting the form, specifying the post_id of the draft post created in step 5.
Request:
- Method:
GET - URL:
http://localhost:8080/public-form-page/?post_id=[DRAFT_ID] - Headers: None required.
Payload:
No payload in the body is required; the payload is the post_id query parameter targeting a non-public post.
7. Expected Results
- Response Status: 200 OK.
- Response Body: The HTML of the form will contain the string:
Title: Secret Draft | Content: FLAG{IDOR_INFO_DISCLOSURE}. - The parser successfully extracted data from a post with
post_status='draft', which is otherwise inaccessible to unauthenticated users.
8. Verification Steps
- Verify Unauthenticated Access: Ensure the request is made without any session cookies or
Authorizationheaders. - Confirm Disclosure: Use
grepor similar logic to find the "FLAG" string in the response body. - Check Post Status: Confirm via WP-CLI that the targeted post is indeed a draft:
wp post get [DRAFT_ID] --field=post_status
9. Alternative Approaches
- Meta Disclosure: If the form contains tags for custom meta keys (e.g.,
{_wp_page_template}or{secret_api_key}), these can also be disclosed by changing the placeholder in the form content. - Password Protected Posts: The same method can be used to extract the content of password-protected posts without providing the password, as
get_post()and raw property access bypasses the password requirement check implemented inthe_content()filters.
Summary
The MW WP Form plugin for WordPress is vulnerable to information exposure via the 'post_id' query parameter in versions up to 5.1.2. Unauthenticated attackers can exploit this to extract content from draft, private, or password-protected posts by pre-filling form data using URL parameters when specific placeholders like {post_content} are present in the form.
Vulnerable Code
// classes/models/class.parser.php:122 protected function _get_post_property_from_querystring( $matches ) { if ( ! isset( $_GET['post_id'] ) || ! MWF_Functions::is_numeric( $_GET['post_id'] ) ) { return; } $post = get_post( $_GET['post_id'] ); if ( empty( $post->ID ) ) { return; } return $this->_get_post_property( $post, $matches[1] ); } --- // classes/models/class.parser.php:166 protected function _get_post_property( $post, $meta_key ) { if ( ! is_a( $post, 'WP_Post' ) ) { return; } if ( isset( $post->$meta_key ) ) { return $post->$meta_key; } $post_meta = get_post_meta( $post->ID, $meta_key, true ); if ( is_array( $post_meta ) ) { return; } return $post_meta; }
Security Fix
@@ -129,7 +129,7 @@ $args = $this->set_names(); if ( empty( $args['shortcode_name'] ) || empty( $args['display_name'] ) ) { - exit( get_class() . '::set_names() returns not right values. Returned values is ' . serialize( $args ) . ' now.' ); + exit( get_class( $this ) . '::set_names() returns not right values. Returned values is ' . serialize( $args ) . ' now.' ); } $this->shortcode_name = $args['shortcode_name']; @@ -88,8 +88,8 @@ */ public function getName() { MWF_Functions::deprecated_message( - get_class() . '::getName()', - get_class() . '::get_name()' + get_class( $this ) . '::getName()', + get_class( $this ) . '::get_name()' ); return $this->get_name(); } @@ -40,7 +40,18 @@ static::$token = ! $saved_token ? static::generate_token() : $saved_token; if ( ! $saved_token && ! headers_sent() ) { $secure = apply_filters( 'mwform_secure_cookie', is_ssl() ); - setcookie( static::KEY, static::$token, 0, COOKIEPATH, COOKIE_DOMAIN, $secure, true ); + setcookie( + static::KEY, + static::$token, + array( + 'expires' => 0, + 'path' => COOKIEPATH, + 'domain' => COOKIE_DOMAIN, + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Lax', + ) + ); } } @@ -134,6 +134,10 @@ return; } + if ( 'publish' !== $post->post_status || post_password_required( $post ) ) { + return; + } + return $this->_get_post_property( $post, $matches[1] ); } @@ -44,7 +44,18 @@ $secure = apply_filters( 'mwform_secure_cookie', is_ssl() ); try { set_error_handler( array( 'MW_WP_Form_Session', 'error_handler' ) ); - setcookie( $this->name, $session_id, 0, COOKIEPATH, COOKIE_DOMAIN, $secure, true ); + setcookie( + $this->name, + $session_id, + array( + 'expires' => 0, + 'path' => COOKIEPATH, + 'domain' => COOKIE_DOMAIN, + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Lax', + ) + ); } catch ( ErrorException $e ) { // No process... } @@ -56,6 +56,7 @@ $filepath = MW_WP_Form_Directory::generate_user_filepath( $form_id, $name, $value ); } + $imagesize = false; if ( file_exists( $filepath ) && exif_imagetype( $filepath ) ) { $imagesize = getimagesize( $filepath ); } else { @@ -70,7 +71,7 @@ 'message' => __( 'This image size is too big.', 'mw-wp-form' ), ); $options = array_merge( $defaults, $options ); - if ( $is_error || $imagesize[0] > $options['width'] || $imagesize[1] > $options['height'] ) { + if ( $is_error || ( is_array( $imagesize ) && ( $imagesize[0] > $options['width'] || $imagesize[1] > $options['height'] ) ) ) { return $options['message']; } } @@ -3,7 +3,7 @@ * Plugin Name: MW WP Form * Plugin URI: https://mw-wp-form.web-soudan.co.jp * Description: MW WP Form is shortcode base contact form plugin. This plugin have many features. For example you can use many validation rules, inquiry data saving, and chart aggregation using saved inquiry data. - * Version: 5.1.2 + * Version: 5.1.3 * Requires at least: 6.0 * Requires PHP: 8.0 * Author: websoudan @@ -80,6 +80,7 @@ add_action( 'admin_menu', array( $this, '_admin_menu_for_chart' ) ); add_action( 'admin_menu', array( $this, '_admin_menu_for_inquiry_data_list' ) ); add_action( 'current_screen', array( $this, '_current_screen' ) ); + new MW_WP_Form_Deprecation_Notice_Controller(); } elseif ( ! is_admin() ) { new MW_WP_Form_Main_Controller(); }
Exploit Outline
The exploit targets forms with the 'querystring' (URL parameter) setting enabled. An unauthenticated attacker identifies a page hosting such a form that uses post placeholders (like {post_content} or {post_title}). By sending a GET request to the form page with the 'post_id' query parameter set to the ID of a draft, private, or password-protected post, the plugin's parser retrieves the unauthorized post object and renders its sensitive properties directly into the form's HTML response.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.