CVE case study

CVE-2026-65523: An Entry ID Exposed Formidable's Private Signature Invite URL

The Formidable Forms signature add-on resolved a request-supplied entry ID to its private signing URL without authentication or an ownership check. Version 2.0.2 derives the entry from signer session state instead.

Severity
High (7.5)
Scoring
CVSS 3.1
Weakness
CWE-639
Affected
Formidable Forms Signature Online Contract Automation 2.0.1 and earlier
Remediation state
Upgrade to version 2.0.2 or later
Advisory published
28 Jul 2026

Official vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Why it matters

The add-on linked a Formidable entry to an e-signature document and its private invite URL. That URL is sensitive because it is the return path issued to the intended signer, not a public document index.

In version 2.0.1, an unauthenticated request could provide frm_esig_agreement as a numeric entry ID. The plugin resolved that ID globally and redirected the caller to the associated invite URL without checking identity, ownership, or signer context.

The official assessment is High at 7.5 for confidentiality. The reproduced boundary was the invite-URL disclosure; this article does not claim that a signature was forged or that every exposed URL remained active.

How I found it

On 17 July 2026, I reviewed the Formidable signature add-on's frontend initialization hooks and searched for request parameters that selected stored agreements. frm_esig_agreement was read during WordPress init on every public request.

I traced that numeric entry ID into getInviteUrl(). The helper mapped the entry to an e-signature document and invitation hashes without checking authentication, session ownership, or signer identity before redirecting.

A disposable WordPress rig with synthetic document metadata returned the private lab invitation URL to an unauthenticated request. I stopped at the redirect and did not view or sign a real document.

Version 2.0.2 removed the entry ID from the browser-controlled return URL and recovered it from the signer's temporary session state, confirming the intended ownership boundary.

Root cause

The plugin instantiated its filter class on every request and attached frm_init() to WordPress init. The handler read the query parameter before any authenticated workflow context was established.

getInviteUrl($entryId) looked up document metadata by the supplied Formidable entry ID, obtained the invite and document hashes, and constructed the private signing URL. The lookup was keyed only by the object identifier.

The intended PayPal return flow already maintained a temporary entry ID in signer session state. Passing the same ID through the URL created an unnecessary second source that an outside caller could control.

Source-to-sink trace

  1. 01
    Unauthenticated sourcefrm_init() on the WordPress init hook

    Any request could supply frm_esig_agreement because the filter class was loaded outside an administrative or authenticated boundary.

  2. 02
    Object selectoresig_formidable_get('frm_esig_agreement')

    The query parameter became the Formidable entry ID without being tied to the current signer session.

  3. 03
    Sensitive lookupESIG_FORMIDABLEFORM_SETTING::getInviteUrl($entryId)

    The helper resolved document and invitation hashes by the global entry identifier.

  4. 04
    Disclosure sinkwp_redirect($inviteUrl)

    The private invitation URL was returned in the response Location header.

  5. 05
    Fixed state sourcegetTempEntryId() in 2.0.2

    The patched handler ignores a direct entry-ID parameter and resolves only the entry stored by the legitimate signer return flow.

Safe proof of concept

Prerequisites

  • Public plugin source for versions 2.0.1 and 2.0.2.
  • Python 3 for a local object-ownership model.
  • No production contract, signer identity, live invite hash, or external request.

Step-by-step reproduction

  1. Compare frm_init() and the PayPal return filter between 2.0.1 and 2.0.2.
  2. Confirm that 2.0.1 reads an entry ID directly from the request, while 2.0.2 accepts only an auth marker and calls getTempEntryId().
  3. Run the synthetic model below with two invented entries. The caller supplies entry 202 while its own session contains entry 101.
  4. Observe that the affected policy returns the other entry's dummy invite path. The fixed policy ignores that request ID and returns only the session-bound path.
  5. Delete the session entry and confirm that the fixed policy returns no redirect.

Compare the public return handlers

LAB_DIR=$(mktemp -d "\${TMPDIR:-/tmp}/formidable-signature-cve.XXXXXX")

curl -fsSL \
  https://plugins.svn.wordpress.org/forms-signature-formidable-online-contract-automation/tags/2.0.1/admin/esig-formidableform-filters.php \
  -o "$LAB_DIR/affected.php"
curl -fsSL \
  https://plugins.svn.wordpress.org/forms-signature-formidable-online-contract-automation/tags/2.0.2/admin/esig-formidableform-filters.php \
  -o "$LAB_DIR/fixed.php"

diff -u "$LAB_DIR/affected.php" "$LAB_DIR/fixed.php"

Synthetic entry-to-invite ownership model

invites = {
    101: "/lab/invite/OWN_SESSION_ENTRY",
    202: "/lab/invite/OTHER_SYNTHETIC_ENTRY",
}

def affected(request_entry):
    return invites.get(request_entry)

def fixed(session_entry):
    return invites.get(session_entry) if session_entry else None

print("affected request 202:", affected(202))
print("fixed session 101:  ", fixed(101))
print("fixed no session:   ", fixed(None))

Expected evidence

  • The affected handler accepts the request-controlled entry ID and reaches the other synthetic invite.
  • The fixed handler returns the invite associated with the established session rather than the request ID.
  • With no session-bound entry, the fixed handler produces no redirect.
Negative control

Use the request ID that matches the synthetic session entry. Both policies resolve the same dummy path, confirming that the repair preserves the intended signer return while eliminating cross-entry selection.

Fixed-version re-test

On version 2.0.2 or later, a direct frm_esig_agreement request must be ignored. Only a legitimate return flow with a current temporary entry in that same session may receive its own invite redirect.

Impact

An unauthenticated caller who guessed or enumerated an existing entry ID could receive an HTTP redirect containing the corresponding private invite URL. The record treats the resulting contract and signer-data exposure as High confidentiality impact.

Availability was unaffected and the public CVE does not claim modification. A valid test therefore stops after recording the redirect destination for a synthetic document in an owned lab.

Fix and retest

Upgrade to version 2.0.2 or later. The fixed frm_init() accepts only the auth return marker, loads the entry ID from the signer's temporary session state, and deletes that state after use.

The PayPal return-URL filter now transports only auth=1; it no longer exposes or trusts the Formidable entry ID in the URL. The server chooses the object from the established workflow context.

Retest with a synthetic agreement. A direct request carrying the old entry-ID parameter must no longer redirect, while the legitimate same-session return flow should still reach its own invite URL exactly once.

Engineering lesson

An object identifier is not proof that the requester may resolve the object. Private links should be derived from authenticated or signer-bound state, not from a globally enumerable request key.

When a multi-step integration already stores server-side state, keep the object identity there. A boolean return marker is safer than reflecting the sensitive identifier through a browser-controlled URL.

References

Follow this thread

Related research, source, and writing.

These paths share a published research boundary, project source, article series, or authorized lab context with this record.

Back to article start