CVE-2026-107431 The two zeroes that compared equal in JetFormBuilder

Intrudify's autonomous pentesting engine discovered a missing authorization vulnerability in JetFormBuilder, affecting all versions up to and including 3.6.6. The plugin decides whether a form submission may update a post by comparing the post's author id with the current user id, and both of those are zero for a logged-out visitor and an anonymously created post. Fixed in 3.6.6.1.

Discovered by
Intrudify autonomous engine
Validated & reported by
Disclosure
12 October 2026
Severity
High · CVSS 7.5
Vulnerability class
Missing authorization, CWE-862
Affected
JetFormBuilder ≤ 3.6.6
Fixed in
3.6.6.1
Active installs
80,000+
Attacker privilege
None, unauthenticated
Reached
Any post created by an anonymous form submission
Execution
Public form submissions, no interaction

Summary

JetFormBuilder builds forms on about 80,000 WordPress sites. Its Insert/Update Post action can create a post from a submission, and can update an existing one when a form field is mapped to the post ID.

Letting a visitor name the post to update obviously needs an ownership check, and there is one. It asks whether the post's post_author equals the current user id.

A logged-out visitor's user id is 0. A post created by an anonymous form submission has post_author = 0. The check therefore passes for every such post, for every anonymous visitor, including posts somebody else submitted.

That is the defect this CVE covers, and it is worth being precise about the history, because the plugin changed twice. The chain originally reported against 3.6.5.4 combined it with the Media field and the media-cleanup routine to delete arbitrary files from the Media Library. 3.6.6 closed the deletion primitive and left the authorization defect open, in writing. 3.6.6.1 closes the authorization defect.

Technical detail

The comparison

// modules/actions-v2/insert-post/properties/post-id-property.php
// JetFormBuilder 3.6.6

$post = get_post( (int) $value );                              // line 40

if (
    ! is_a( $post, \WP_Post::class )
    || (
        absint( $post->post_author ) !== get_current_user_id()      // line 45
        && ! current_user_can( 'edit_post', $post->ID )
        && ! Tools::is_webhook()
    )
) {
    throw new Action_Exception( 'failed', ... );
}

// for a logged-out request get_current_user_id() is 0, and a post
// created by an anonymous submission has post_author = 0. the two
// sides are equal, the branch is false, and the request is allowed
// to update that post.

get_current_user_id() returns 0 when nobody is logged in, and that is documented behaviour, not an edge case. post_author is 0 on a post created by a visitor with no account, which is the ordinary result of the feature this plugin exists to provide. Nothing distinguishes the two zeroes, so the expression that was written to mean "this is your post" means "neither of you is anybody".

The vendor says it plainly

Both halves of this are stated in the plugin's own shipped code, which makes the reachability unusually well evidenced:

// the vendor's own words, shipped in 3.6.6's media-cleanup module,
// explaining why the cleanup path deliberately refuses to rely on
// the same match:

//   "a logged-out request is deliberately NEVER authorized here, even
//    though can_attach() currently authorizes it via
//    post_author === 0 === get_current_user_id() (issues-tracker
//    #20547's still-open Defect 3 ... not fixed by this file). That
//    anonymous match is not a verifiable identity: because Defect 3
//    lets ANY anonymous visitor get authorized on ANY post_author = 0
//    post, not just one they created, relying on it here would let
//    an attacker delete a stranger's anonymously-uploaded attachment
//    ..."


// and in 3.6.6.1, on the line that closes it:

//   "post_author 0 (anonymous post) must not match a logged-out
//    visitor, whose user ID is also 0."

The first comment is in 3.6.6, in the module that had just been hardened. It is the vendor explaining that they would not rely on the anonymous match, because that match lets any anonymous visitor be authorized on any post_author = 0 post, and naming the defect as still open.

The originally reported chain

Against 3.6.5.4 the ownership defect was one of three parts. The Media field returns the submitted value when no file is uploaded, so a visitor could put any attachment id into the post meta; and the media-cleanup routine deleted any attachment id that disappeared from that meta between two submissions:

# the chain as reported against 3.6.5.4, in three anonymous requests

# 1. create a carrier post through the public submit form.
#    it is stored with post_author = 0.
POST /?page_id=<submit page>
    _wpnonce=<public nonce>&_jet_engine_booking_form_id=<submit form>
    &ttl=attacker+listing
-> 302 ...?inserted_post_id=N&status=success

# 2. point the Media field at somebody else's attachment.
#    no file is uploaded, so the field returns the submitted value.
POST <edit form action>
    &pid=N&photo[]=8
-> post meta gallery = array( 0 => '8' )

# 3. submit again with the field empty. the cleanup routine diffs
#    the attachment ids before and after and deletes what vanished.
POST <edit form action>
    &pid=N&photo=
-> wp_delete_attachment( 8, true )

# attachment 8 and its file are gone. ids are sequential, so a loop
# over the range reaches the whole Media Library.
// modules/media-cleanup/module.php, JetFormBuilder 3.6.5.4
// the whole file is 162 lines. this is the delete loop in full.

foreach ( $attachment_ids as $attachment_id ) {            // line 104

    if ( 'attachment' !== get_post_type( $attachment_id ) ) {
        continue;                                      // the only check
    }

    wp_delete_attachment( $attachment_id, true );       // line 109
}

Verification

This one is reported differently from the rest of these pages, because the interesting work was a comparison across three releases, and because one claim here is read from source instead of driven over HTTP. What was measured and what was not is marked throughout.

Measured: which guards exist in which release

Three releases were downloaded from wordpress.org and read side by side:

# guards present before wp_delete_attachment(), read out of each
# release's own copy of modules/media-cleanup/module.php

# version   file    authorize_   _jfb_uploaded   is_owned_by_
#           lines   context_post  _by_form        current_actor

  3.6.5.4     162     0             0              0
  3.6.6       434     6             4              6
  3.6.6.1     489     6             4              6

# so the deletion primitive the report demonstrated was closed in
# 3.6.6, together with a three-phase migration that backfills the
# new ownership marker onto attachments uploaded before it existed.


# what 3.6.6.1 changes, by file:

  post-id-property.php    Defect 3: the anonymous author match
  file-uploader.php       is_permitted() made public, so a submission
                          with no file can be checked the same way
  media-cleanup/module.php  locked media fields are skipped entirely,
                          and their stored value is left alone
  post-meta-property.php  a Default Field cannot overwrite a locked key

That table is the substance of this advisory. The file deletion the report demonstrated is not reachable on 3.6.6: the cleanup routine rejects every logged-out request outright, refuses any attachment the plugin did not upload itself, and checks ownership of the attachment. 3.6.6 also shipped a three-phase migration that backfills the new ownership marker onto attachments that predate it, so the guard works on existing libraries.

What remained on 3.6.6, and what 3.6.6.1 fixes, is the authorization defect itself and the capability gap on a submission that uploads no file.

What was read from source

The behaviour of the comparison on line 45 is read from the source and corroborated by the two vendor comments quoted above, one of which describes its exploitability in the vendor's own words for the exact version this CVE covers. This lab did not re-drive the end-to-end chain over HTTP. A public form with an Insert/Update Post action was built on 3.6.6 and rendered correctly, and submissions failed in the harness before reaching the action, which is a fact about the harness and not about the plugin. It is reported here so the evidence for each claim is stated.

The originally reported three-request chain against 3.6.5.4, including the deleted attachment and the removed file, is the report's own measurement.

Impact

The attacker needs no account, no token and no victim interaction. The requirement is a public JetFormBuilder form with an Insert/Update Post action whose ID is mapped to a field, which is the standard way the plugin's own documentation describes building a directory or classifieds site where visitors edit their own listings.

On an affected release every post created by an anonymous submission is writable by every anonymous visitor. On a site built that way, that is the whole set of listings: anyone can rewrite anyone else's entry, and any field the form maps can be changed.

On 3.6.5.4 the same defect was the authorization half of an arbitrary file deletion. wp_delete_attachment() was called with force, removing the original and every generated size, and attachment ids are sequential, so a loop over the range reaches the entire Media Library. That half was closed in 3.6.6.

The two databases score this one very differently, and the gap is instructive. Patchstack scored 7.5 on CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H, where the availability metric carries the file deletion. Wordfence scored 5.3 on the same vector with I:L and A:N, which fits the residue that remains at the published ceiling of 3.6.6. Both are defensible readings of a finding whose impact changed between the report and the disclosure.

Remediation

Update JetFormBuilder to 3.6.6.1 or later. A site still on 3.6.5.4 or earlier should treat this as urgent, because the file deletion is live there.

// JetFormBuilder 3.6.6.1

+ // post_author 0 (anonymous post) must not match a logged-out visitor,
+ // whose user ID is also 0.
  if (
      ! is_a( $post, \WP_Post::class )
      || (
-         absint( $post->post_author ) !== get_current_user_id()
+         ! ( is_user_logged_in() && absint( $post->post_author ) === get_current_user_id() )
          && ! current_user_can( 'edit_post', $post->ID )
          && ! Tools::is_webhook()
      )
  ) {


// and the capability that was only checked when a file was uploaded

+ public static function is_permitted( array $settings ): bool {
+     $cap = ! empty( $settings['allowed_user_cap'] ) ? ... : 'upload_files';
+     if ( 'any_user' !== $cap && ! is_user_logged_in() ) {
+         return false;
+     }
+     ...
+ }

+ // "File_Uploader checks that capability only when a file is uploaded,
+ //  so a submission without a file would otherwise still be able to
+ //  write attachment IDs. The value is not emptied: a permission
+ //  failure must never turn an unchanged field into a removal request"

The authorization fix is the right shape: is_user_logged_in() is required before the author comparison is allowed to mean anything, so a logged-out request can no longer match a zero author. The capability fix extracts the permission check so a submission that uploads no file is held to the same standard as one that does, and the cleanup routine now skips a field the submitter may not change instead of treating it as emptied.

That last detail is worth copying. The comment says it directly: a permission failure must never turn an unchanged field into a removal request. A guard that fails by blanking a value turns an authorization error into a destructive action, which is the worst way for a check to fail.

If a site ran 3.6.5.4 or earlier with a public edit form, assume media may have been deleted and restore from backup. The 3.6.6 migration marks attachments the plugin uploaded, but it cannot bring back files that were already removed.

The wider lesson

Zero is the most dangerous value in a WordPress authorization check. get_current_user_id() returns it for a logged-out request, post_author carries it for an anonymously created post, and get_post_meta() returns an empty string that will happily compare equal to it under ==. Every one of those is documented and sensible on its own, and comparing any two of them produces a check that passes in exactly the situation it was written to catch.

So when an equality test decides authorization, ask what each side is when nobody is there. If both sides can be absent, the check is strongest against real users and weakest against anonymous ones, which is backwards. The fix here is the general form of the answer: establish that there is an identity before comparing identities.

The second thing is about reading a fix in stages. This plugin was patched twice, and the first patch deliberately left the authorization defect open while hardening everything downstream of it. The vendor documented that choice carefully and at length, which is more than most do, and the comment they wrote to justify it is also a precise description of how to exploit what they had left in place. A patch that explains why it is not fixing something is telling you exactly where to look next, which is the same lesson a Presto Player patch comment taught in this research set.

Last, a practical note for anyone auditing a plugin from a report: check which release the report was written against. The deletion chain here is real and was demonstrated, and it describes a version two releases behind the one the CVE names. Repeating the report's steps on the current release and seeing nothing happen would be the wrong conclusion drawn from a correct observation.

Disclosure timeline

  • 2026-09-24 Vulnerability identified and confirmed by Intrudify against 3.6.5.4, reported via Patchstack
  • 2026-09-29 Release 3.6.6 published by Crocoblock, closing the file deletion primitive
  • 2026-10-08 Release 3.6.6.1 published, closing the authorization defect
  • 2026-10-12 Public disclosure by Patchstack
  • 2026-10-14 CVE-2026-107431 published

Questions

What is CVE-2026-107431?

CVE-2026-107431 is a missing authorization vulnerability in JetFormBuilder, affecting all versions up to and including 3.6.6. The Insert/Update Post action decides whether a submission may update a post by comparing the post's author id with the current user id. Both are zero for a logged-out visitor and a post created by an anonymous submission, so any anonymous visitor is authorized on any such post.

Who can exploit CVE-2026-107431?

Anyone who can reach the site. No account, no token and no victim interaction are needed. The site has to have a public form with an Insert/Update Post action whose ID is mapped to a field, which is the standard setup for letting visitors edit their own submissions.

Can this delete files from my Media Library?

On 3.6.5.4 and earlier, yes: the same defect was the authorization half of a chain that called wp_delete_attachment() with force on any attachment id. Release 3.6.6 closed that by requiring a logged-in request, refusing any attachment the plugin did not upload, and checking ownership. Sites still on 3.6.5.4 or earlier should update urgently and check their Media Library against a backup.

Why do the two vulnerability databases score this so differently?

Because the impact changed between the report and the disclosure. Patchstack's 7.5 carries an availability metric reflecting the file deletion demonstrated against 3.6.5.4. Wordfence's 5.3 has no availability impact and fits what remains at the published ceiling of 3.6.6, where the deletion primitive was already closed and the authorization defect was not.

How do I fix CVE-2026-107431?

Update JetFormBuilder to 3.6.6.1 or later. That release requires is_user_logged_in() before the post author comparison can authorize anything, and applies the Media field's user-access capability to submissions that upload no file, skipping a field the submitter may not change instead of treating it as emptied.

References

More discovered CVEs from Intrudify

See what a pentest
finds in your app.