A form field that treats a serialized attacker string like a signed contract with no signature check
CVE-2026-103039 affects Gravity Forms for WordPress up to and including 3.1.2. When a form contains a Post Custom Field with an input type of List and a content template enabled, GFFormsModel::create_post() passes the submitted value to WordPress' unrestricted maybe_unserialize() instead of the plugin's own GFCommon::maybe_unserialize(allowed_classes => false). That means an unauthenticated attacker can inject a PHP object into post meta.
The vendor labels this CRITICAL at 9.8, but the plugin itself ships no PHP object injection gadget chain. A concrete RCE path exists only when the field is mapped to the protected _wp_page_template meta key, the host has a readable PEAR install, and register_argc_argv is enabled—or when a separate plugin/theme supplies a gadget chain. Those preconditions are real but narrow, so the practical severity is HIGH rather than CRITICAL.
5 steps from start to impact.
Identify a vulnerable Gravity Forms form
- Gravity Forms ≤ 3.1.2 installed
- Form includes a Post Custom Field with List input type
- Content template enabled on that field
- Form is publicly accessible
- Most Gravity Forms deployments do not use this exact field configuration
- Forms may have CAPTCHA, honeypot, or anti-spam protections
- Automated scanning for form configuration is not straightforward
Craft a serialized PHP object payload
validate_file() accepting non-scalars and WP_Theme::__toString() resolving an attacker-controlled path. Alternatively, the attacker uses a POP chain from an installed plugin or theme.- Knowledge of target stack and installed plugins/themes
- A gadget chain available either in WordPress core (with specific host config) or in a third-party component
- Gravity Forms has no native gadget chain
- The WordPress core chain requires a specific admin mapping to
_wp_page_templateplus PEAR andregister_argc_argv - Third-party gadget chains vary widely and may not be present
Submit malicious serialized value as unauthenticated user
maybe_unserialize() call inside create_post().- Form accepts public submissions
- No authentication requirement on the form
- Payload passes any client-side validation
- WAF rules for PHP object injection may block
O:ora:serialized strings - Gravity Forms honeypot/spam filtering can reject automated submissions
- Rate limiting or IP reputation may slow large-scale exploitation
Trigger object instantiation via unsafe unserialize
maybe_unserialize() instantiates attacker-controlled classes and invokes magic methods. In the WordPress core chain, the eventual path depends on WP_Theme::__toString() and validate_file() handling non-scalars, which only matters when the meta key is _wp_page_template.- Custom field mapped to
_wp_page_template(admin-configured reserved meta key) - Serialized object matches the gadget chain
- PHP configuration allows object instantiation (default)
- Mapping a field to
_wp_page_templateis an uncommon admin misconfiguration - Without that mapping, the core chain does not fire; third-party chain becomes the only path
Achieve remote code execution on the web server
register_argc_argv enabled, the resolved path from WP_Theme::__toString() leads to code execution under the web server user. Alternatively, a third-party gadget chain may lead straight to RCE.- PEAR library readable by the PHP process
- PHP setting
register_argc_argv = On - Web server user has permissions to execute the resulting payload
- Many PHP-FPM production configs disable
register_argc_argv - PEAR is often absent on minimal or hardened WordPress hosts
- Shared hosting may sandbox PHP and prevent cross-site impact
maybe_unserialize() call with GFCommon::maybe_unserialize(allowed_classes => false), eliminating object instantiation. Apply within 30 days per noisgate mitigation SLA for HIGH; final remediation deadline is 180 days.O: or a: serialized strings in Gravity Forms submission fields. This reduces exposure while patch testing. Deploy within 30 days per HIGH mitigation SLA._wp_page_template Post Custom Field mappings — Check all forms for a custom field named _wp_page_template with List input and content template enabled. Remove or rename these fields. This directly blocks the WordPress core gadget chain.- Disabling
register_argc_argvalone: stops the WordPress core chain but does not prevent third-party gadget chains. - Updating WordPress core: the vulnerable call is in Gravity Forms, not WordPress itself.
- Restricting PEAR file permissions: blocks one core chain pathway but leaves third-party gadget exploitation intact.
The supporting signals.
| In-the-wild status | No known active exploitation; not listed in CISA KEV as of this assessment. |
|---|---|
| Proof-of-concept availability | No public PoC repo named after CVE-2026-103039 found on GitHub; conceptual payloads for the WordPress core chain are discussed in WordPress security research. Check pocindex.io before acting. |
| EPSS score | 0.00508 (0.508% predicted exploitation probability in next 30 days) — very low predictive signal, but EPSS may lag unreported plugin vulnerabilities. |
| KEV status | Not listed in Cisco/CISA KEV catalog. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8; assumes a gadget chain is always available, which is not true for the plugin itself. |
| Affected versions | Gravity Forms <= 3.1.2. |
| Fixed versions | 3.1.3 or later (first release after 3.1.2; verify against vendor changelog). |
| Scanning / exposure data | Shodan/Censys cannot fingerprint Gravity Forms version or form configuration; public forms are exposed, but the vulnerable field setup is not externally detectable. |
| Disclosure date | 2026-10-30 |
| Reporting researcher/org | Not specified in the official CVE record; likely coordinated through Patchstack or Wordfence. |
Sources.
Why this verdict
- Unadjusted CVSS: vendor scores 9.8 because the attack is unauthenticated, remote, and can lead to RCE, but that assumes a gadget chain is permanently present in the vulnerable component.
- Gadget dependency: Gravity Forms itself ships no POP chain; real damage requires either a third-party plugin/theme gadget or the WordPress core chain, which only fires under specific conditions.
- WordPress core chain friction: the concrete RCE path requires
_wp_page_templatemapping, a readable PEAR install, andregister_argc_argvenabled. Each precondition is uncommon, and all three together are rare. - Role multiplier: WordPress/Gravity Forms runs in the exposed web tier. If RCE lands on a commerce or membership site, the blast radius includes customer PII and site takeover, but the affected component is not canonically a high-value role like IdP, hypervisor, or DC. No CRITICAL floor applies from deployment role alone.
- Exposure: only forms with Post Custom Field List + content template are reachable; a small subset of Gravity Forms installs meet this configuration.
- Detection friction: serialized PHP object payloads are noisy; WAFs and Gravity Forms honeypots can block many automated attempts.
Why not higher?
We do not keep CRITICAL because the plugin itself lacks a gadget chain, and the only in-core RCE path requires a misconfigured reserved meta key plus two host-level conditions. The attack is not fire-and-forget against a default Gravity Forms install; it requires environment-specific reconnaissance and a matching gadget.
Why not lower?
We do not drop to MEDIUM because the injection is unauthenticated, Gravity Forms is widely exposed on public sites, and the WordPress core chain gives a concrete RCE outcome on some real hosts. Compromise of a web server hosting customer data or e-commerce flows warrants HIGH even without a universal exploit.
Crowdsourced verification payload.
Run on the WordPress web host as the web user or a user with read access to wp-content/plugins/gravityforms/. Invoke as bash check_cve-2026-103039.sh /path/to/wordpress with the WordPress root path. Requires read access to plugin files.
#!/bin/bash
# Check for CVE-2026-103039 in Gravity Forms plugin
WP_ROOT="${1:-.}"
if [ ! -f "$WP_ROOT/wp-content/plugins/gravityforms/gravityforms.php" ]; then
echo "UNKNOWN: Gravity Forms plugin not found at $WP_ROOT"
exit 3
fi
GF_VERSION=$(grep -m1 'Version:' "$WP_ROOT/wp-content/plugins/gravityforms/gravityforms.php" | awk '{print $2}' | tr -d '\r')
if [ -z "$GF_VERSION" ]; then
echo "UNKNOWN: could not determine Gravity Forms version"
exit 3
fi
# Compare version to 3.1.3
MAJOR=$(echo "$GF_VERSION" | cut -d. -f1)
MINOR=$(echo "$GF_VERSION" | cut -d. -f2)
PATCH=$(echo "$GF_VERSION" | cut -d. -f3)
if [ "$MAJOR" -lt 3 ] || { [ "$MAJOR" -eq 3 ] && [ "$MINOR" -lt 1 ]; } || { [ "$MAJOR" -eq 3 ] && [ "$MINOR" -eq 1 ] && [ "$PATCH" -le 2 ]; }; then
echo "VULNERABLE: Gravity Forms version $GF_VERSION <= 3.1.2"
exit 1
else
echo "PATCHED: Gravity Forms version $GF_VERSION >= 3.1.3"
exit 0
fi