Automated Threat Modeling

Gravity Forms CVE-2026-103039: Unauthenticated PHP Object Injection with a Public PoC

A summary of CVE-2026-103039, an unauthenticated PHP object injection flaw in Gravity Forms through 3.1.2, with technical analysis, a public PoC review, and the patch details for version 3.1.3.

CVE Analysis

9 min read

Gaurav Sarraf
Gaurav Sarraf

2026-11-03

Gravity Forms CVE-2026-103039: Unauthenticated PHP Object Injection with a Public PoC
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A form plugin running on more than 5 million websites shipped a safe deserialization helper, then kept calling the unsafe one in a code path that accepts unauthenticated submissions. That gap is CVE-2026-103039, a PHP object injection flaw in Gravity Forms that lets an unauthenticated attacker plant a live PHP object into post metadata, with conditional paths toward remote code execution depending on what else is installed.

Gravity Forms, developed by Rocketgenius, Inc., is a commercial form management system for WordPress used to collect, manage, and share business data. The vendor states the product powers more than 5 million websites worldwide. Because it processes attacker-reachable input by design, a deserialization defect in its submission handling matters across a lot of sites.

Technical Information

The weakness is CWE-502, Deserialization of Untrusted Data. WordPress core exposes a global function, maybe_unserialize(). If a string looks serialized, that function hands it to PHP's unserialize() with no class restriction. A string shaped like O:8:"SomeClass":... therefore becomes a real object, and that object's magic methods (__unserialize() or __wakeup()) can run later.

In version 3.1.1, Gravity Forms added a safer static method with an almost identical name:

GFCommon::maybe_unserialize( $data, $allowed_classes = false )

Its documentation describes unserializing data without instantiating PHP objects by default. The false default is forwarded as allowed_classes, so PHP turns any embedded object into an inert __PHP_Incomplete_Class instead of the attacker's chosen class.

The defect is that GFFormsModel::create_post() still called the unsafe WordPress global when handling a Post Custom Field with the List input type. Wordfence notes that when a content template is enabled, the submitted row reaches that call unchanged. The result is an object injection primitive reachable with no authentication and no user interaction, consistent with the CVSS 9.8 vector.

What the primitive does and does not give you

The injection point lets an attacker place a live PHP object into post meta. On its own it does not establish code execution. Gravity Forms contains no applicable gadget chain for this issue. The advisory documents two routes that can escalate the primitive:

  1. An additional plugin or theme supplies a usable property oriented programming (POP) chain. OWASP explains that PHP object injection becomes actionable when available classes and magic methods provide useful behavior, and that those classes must be declared or autoloadable at deserialization time.
  2. The custom field is mapped to the protected _wp_page_template meta key. In that case WordPress itself supplies the chain: validate_file() accepts non-scalars, WP_Theme::__toString() resolves an attacker-controlled path, and on hosts with a readable PEAR installation and register_argc_argv enabled this reaches remote code execution.

The 9.8 rating describes severe potential impact, not an observed compromise of every affected site. NVD displays Wordfence's score and has not supplied its own independent assessment.

Attack flow

Based on the briefing, an unauthenticated attacker would:

  1. Identify a Gravity Forms installation at or below version 3.1.2.
  2. Locate a form containing a Post Custom Field with the List input type and a content template enabled.
  3. Extract the form submission nonce (_gform_submit_nonce_<form_id>).
  4. Submit a serialized PHP object as the List field value, which reaches the unrestricted maybe_unserialize() call in create_post() and is instantiated into post meta.
  5. Depending on available gadget chains or the _wp_page_template mapping and host configuration, achieve conditional code execution.

Proof of Concept

A public proof-of-concept exists in the GitHub repository noxcrypt-x/CVE-2026-103039, created on October 31, 2026, written in Python by the handle "NXCRYPT". The repository contains a single working PoC script, CVE-2026-103039.py, plus a README.md. It is reviewable but not a self-contained end-to-end RCE exploit: the optional PHP payload generator gen_payload.php is left out on purpose (confirmed by .gitignore and the README), so the operator must supply a serialized PHP gadget-chain payload via --payload or --payload-file. The author claims no live target validation.

From the script source, it implements:

  • Version detection via wp-content/plugins/gravityforms/readme.txt (parsing Stable tag:) with a JS ?ver= fallback, comparing against VULNERABLE_MAX = (3, 1, 2).
  • Form and field auto-discovery by regexing gform_<id>, data-formid, and field_<form>_<field> / gfield--type-<t> from candidate pages (/, /contact/, /contact-us/, /?gf_page=preview&id=N).
  • Nonce extraction of _gform_submit_nonce_<form_id>.
  • Payload submission as a form POST, setting both input_<field_id> and input_<field_id>[0][0] to the serialized payload.
  • Optional CAPTCHA handling (manual answer, cached replay, pytesseract OCR, wordlist brute, or 2Captcha API).
  • RCE verification by scanning responses and follow-up post URLs for markers uid=\d+, gid=\d+, and the sentinel GF_CVE_2026_103039_OK.
  • Multi-target threading, field scanning (--scan-fields), and JSON output.

The submission wiring in the script is:

data = { "gform_submit": str(form_id), f"is_submit_{form_id}": "1", f"_gform_submit_nonce_{form_id}": nonce, } data[f"input_{field_id}"] = payload data[f"input_{field_id}[0][0]"] = payload

Representative usage from the script's own documentation:

# Version detect python CVE-2026-103039.py -u https://target --detect # Auto-detect form/field IDs python CVE-2026-103039.py -u https://target --auto-detect # Submit a (caller-supplied) serialized payload and verify python CVE-2026-103039.py -u https://target \ --form-id 1 --field-id 2 \ --payload "O:13:\"WP_HTML_Token\":..." \ --form-page https://target/contact/ --verify # Scan all fields of a form python CVE-2026-103039.py -u https://target --form-id 1 --scan-fields # Mass scan python CVE-2026-103039.py -l targets.txt --detect --threads 20 -o results.json

This is a genuine, publicly available PoC and scanner that automates target identification and delivery of the object-injection vector, which lowers the bar for exploitation. But because the gadget-chain payload generator is withheld and no live RCE run is demonstrated, full weaponized unauthenticated RCE still requires the operator to supply a working serialized PHP object gadget chain, and depends on a suitable gadget being present on the target.

Patch Information

The fixed version is Gravity Forms 3.1.3, released October 1, 2026. Wordfence, the CNA, lists 3.1.3 as the patched version and advises updating to 3.1.3 or a newer patched release. The vendor's changelog entry for 3.1.3 only states "Added security enhancements" and does not name the CVE. A third-party mirror of the plugin source (codewurker/gravityforms, commit 32ea71f, "Updates to 3.1.3") shows the exact change. One upstream system listed 3.2.1 as the fix; 3.2.1 is a later release that also covers this bug, but the change first appears in 3.1.3.

The core fix in forms_model.php is a single line:

@@ -5164,7 +5164,7 @@ public static function create_post( $form, &$lead ) { switch ( RGFormsModel::get_input_type( $custom_field ) ) { case 'list': - $value = maybe_unserialize( $value ); + $value = GFCommon::maybe_unserialize( $value ); if ( is_array( $value ) ) {

Normal List data is a serialized array of rows and columns and still decodes correctly, but a serialized object can no longer be turned into a live instance. Because no attacker-controlled object is created, nothing usable reaches post meta, which also blocks the _wp_page_template to WP_Theme::__toString() chain described in the advisory.

The 3.1.3 diff also replaces the global maybe_unserialize() with GFCommon::maybe_unserialize() at several other call sites that read stored List data, closing a second path where a stored payload could come back to life when an entry is read later:

  • includes/webapi/webapi.php: maybe_json_encode_list_fields(), for List field values in the v1 REST API.
  • includes/webapi/v2/includes/controllers/class-gf-rest-controller.php: both maybe_json_encode_list_fields() and prepare_entry_for_response(), for List fields in v2 REST responses.
  • gravityforms.php: decoding the add-ons API response body in install_addon_translations().
  • settings.php: decoding the plugin-info response in install_recaptcha_addon().

For example, in the v2 REST controller:

- $entry[ $field->id ] = maybe_unserialize( $entry[ $field->id ] ); + $entry[ $field->id ] = GFCommon::maybe_unserialize( $entry[ $field->id ] );

The same release also adds GF_Field_List::sanitize_list_value(), which runs wp_kses() over every string cell before List rows are serialized in get_value_save_input(), and moves several field types to a stricter get_entry_allowed_html() policy. Those changes appear to address a separate List-field stored XSS fixed in the same version, not the object injection.

The reviewer lesson is direct: adding a safe helper secures nothing until every old call site is switched over. The hardened function had existed since 3.1.1, and the bug survived because one create_post() branch still used the WordPress global. A quick way to check a patched install is to grep for remaining direct calls to maybe_unserialize( or unserialize( on user-influenced data.

Affected Systems and Versions

  • Affected product: Gravity Forms for WordPress (Rocketgenius, Inc.).
  • Vulnerable versions: all versions up to and including 3.1.2.
  • Vulnerable configuration: a Post Custom Field using the List input type with a content template enabled, so the submitted row reaches the unrestricted maybe_unserialize() call verbatim.
  • Not remediated: versions 3.1.1 and 3.1.2, despite the hardened GFCommon::maybe_unserialize() helper existing since 3.1.1.
  • First patched version: 3.1.3 (released October 1, 2026). Later releases such as 3.2.1 also contain the fix.
  • Conditional RCE prerequisites: a POP gadget chain from another installed plugin or theme, or the _wp_page_template meta-key mapping on a pre-fix WordPress with a readable PEAR install and register_argc_argv enabled.

Vendor Security History

In July 2025, Gravity Forms issued a security incident notice stating that limited manually downloaded packages were maliciously modified: 2.9.11.1, downloaded on July 9 or 10, and 2.9.12, downloaded on July 10. Automatic updates to 2.9.12 through the WordPress dashboard and packages downloaded on other dates were not affected. The modifications attempted to block updates, contact an external server for additional payloads, and potentially add an administrative account. The vendor responded by releasing 2.9.13, scanning other downloadable packages, updating service credentials, auditing administrative accounts, cycling passwords, and working to disable associated infrastructure.

That incident concerns compromised software distribution, while CVE-2026-103039 concerns processing an attacker submission through vulnerable application code. They are distinct and should not be treated as the same campaign.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Gerrit
  • CVS
  • Jira
  • Linear
  • Slack
  • Security Compass
Explore integrations
Security magnifying glass visualization