Blog / CVE explainers

CVE-2026-85097: Bricksforge trusts the file you name

CVE-2026-85097 lets an anonymous visitor plant a PHP shell through a Bricksforge form. Here's the trust split behind it, and how to check your own uploads.

Completed doctoral studies in Information SecurityUniversity defense passed · Final dissertation defense pending

One POST to a contact form, and the server runs your code

One POST to a form. That is the distance between an anonymous visitor and code running on your server. The request below is the whole bug, and it is already being fired at live sites. CVE-2026-85097 is an unauthenticated arbitrary file upload in Bricksforge, a popular add-on for the Bricks site builder, affecting every version up to and including 3.1.8.9 and fixed from 3.1.8.10. [1][2] Wordfence, which assigned the CVE, scores it 9.8. Patchstack, which caught it being exploited, calls it a 10.0. [1][2]

Bricksforge was notified on 3 September, and the details went public on 7 October. [2] Patchstack saw the first exploitation attempt the same day, at 21:47 UTC, from automated infrastructure that was clearly waiting. [1] I have not run this against a live install. What follows is built from Patchstack’s write-up and the code it published, the Wordfence advisory, the vendor’s changelog and the WordPress docs, read on 12 October 2026. [1][2][3][4] The interesting part is not that a file upload led to code execution. It is that the plugin checked the file carefully and got owned anyway.

POST /wp-json/bricksforge/v1/form_submit HTTP/1.1
Host: victim.example
Content-Type: application/json

{
  "temporaryFileUploads": [
    {
      "field": "form-field-file",
      "file": {
        "file": "/var/www/.../uploads/bricksforge/tmp/img.gif",   // real GIF, passed the MIME check
        "url":  "https://victim.example/wp-content/uploads/shell.php", // attacker names the destination
        "type": "image/gif"
      }
    }
  ]
}

The server checked the bytes. The attacker named the file

Here is the design Bricksforge trusted. When a visitor attaches a file, the plugin uploads it to a temporary folder and checks its MIME type there, properly, before anything else happens. A GIF has to really be a GIF, and that check works. The developers then did something that feels harmless. When the form is finally submitted, they let the browser tell the server which file it meant and where that file should end up.

That is the whole mistake, and “it doesn’t validate the url field” undersells it. The plugin validated one property of the file, its contents, on one field. Then it chose the file’s final name and extension from a different field that the attacker controls and that is never re-checked. The thing it inspected and the thing it acted on were not the same thing. A GIF’s bytes tell you nothing about whether the file you are about to write will be called shell.php, and on an Apache-plus-mod_php host it is the .php on the end, not the image header, that decides whether the server runs it.

A valid image, copied into a file that executes

Walk the two stages. First the upload. The attacker asks an unauthenticated endpoint for a nonce, then uploads a polyglot: a file that is a valid GIF and also contains a line of PHP. Because it is a real image, mime_content_type reports image/gif, and it lands in the temporary directory untouched. [1] So far the plugin has done everything right. The file on disk is genuinely an image.

Then the form is submitted, and the metadata does the work. The submitted JSON carries a file object with two fields that matter: file, the server path of the thing that was validated, and url, where it should go. The code below is the vulnerable path, condensed from what Patchstack published. [1] The destination name is taken from the url field, run through basename, and used as the name to copy the validated bytes into.

Read what that copy() does. It takes the real GIF sitting in the temp directory and writes its bytes to a path the attacker named, ending in .php, inside the web root. The image header keeps the file a valid GIF; the PHP inside it is what the server executes the first time that URL is requested. [1] The MIME check never had a chance, because it was answering a question nobody was asking. It proved the file was an image. It never proved the file would be stored, or served, as one.

// Bricksforge Pro Forms (condensed from the vulnerable path)
$file_path = $file_data['file'];   // server path of the file that was MIME-checked
$file_url  = $file_data['url'];    // client-supplied, never re-validated

$attachment['guid'] = $file_url;                       // destination comes from the URL
$desired_filename   = basename($attachment['guid']);   // ...so from the attacker
$destination_path   = $upload_path . '/' . $desired_filename;

copy($file_path, $destination_path);   // GIF bytes written to an attacker-named .php

Why “our forms have no upload field” does not save you

Now the part that should bother you even if you don’t think you’re exposed. You might assume this only touches forms that accept uploads. Bricksforge’s own changelog says otherwise: the flaw affects every site with Pro Forms active, including forms that use no upload fields at all. [3] The reason is in the code. The list of files to process is not read from a real multipart upload. It is read from the temporaryFileUploads value in the JSON body, which the attacker writes from scratch. [1] A plain text contact form is enough.

And the nonce is not the gate it looks like. Bricksforge exposes an endpoint, bricksforge_regenerate_nonce, that hands a fresh nonce to any anonymous visitor, which already defeats the point. Even without it, WordPress generates the same nonce for every logged-out user, because they all share user ID 0. [4] A nonce proves a request came from a page, not from a trusted person. The WordPress documentation puts it in one line: nonces should never be relied on for authentication, authorization, or access control. [4] This handler leaned on one for exactly that.

// The file list is read from the JSON body, not from a real multipart upload:
$file_uploads = isset($form_data['temporaryFileUploads'])
    ? json_decode($form_data['temporaryFileUploads'], true)   // attacker-supplied
    : [];
$merged_files = array_merge($file_uploads, $request->get_file_params());

// The gate in front of it:
//   GET bricksforge_regenerate_nonce  -> hands any anonymous visitor a nonce
//   WordPress gives every logged-out user the SAME nonce (user ID 0)
// A nonce is CSRF hygiene. It was never authentication.

The fix gives the filename back to the server

The fix restores the obvious rule: the server, not the client, decides where an uploaded file goes and what it is called. Patchstack and the Wordfence advisory name 3.1.8.10 as the fixed build. [1][2] Bricksforge’s changelog lists the file-upload fix under 4.0.1, released 8 October, and says full details follow after disclosure. [3] Update to whichever your license gives you, but treat 3.1.8.10 as the floor.

The principle underneath outlives this plugin. A stored filename is a security decision, so the server has to own it: generate a random name, pin the extension to an allowlist, and store outside the web root or somewhere the server will not run PHP. The block below is that shape, not the vendor’s diff. There is also a distribution trap specific to Bricksforge. It is a commercial plugin, not on WordPress.org, updated through a license. [3] A lapsed license can silently leave you on a vulnerable build while your dashboard looks fine. “We bought it once” is not “we are patched”.

// The fix, as a principle: the SERVER owns the name and extension, never the client.
$ext         = 'bin';                             // or an allowlisted image extension
$stored      = wp_generate_uuid4() . '.' . $ext;  // random name the attacker can't choose
$destination = $upload_path . '/' . $stored;
copy($validated_tmp_file, $destination);

// And decide execution by WRITE location, not by MIME: store outside the web root,
// or in a directory where the server is configured never to run PHP.

How I’d find this in code that isn’t Bricksforge

This bug is a class, and the class is older than WordPress: validate one property, act on another. Whenever an upload handler checks a file’s contents but lets request input choose the file’s name, extension or directory, you have this hole, whether the stack is PHP, Node or Java. When I review an upload path, I don’t start at the MIME check everyone is proud of. I start at the write.

I find every sink that puts bytes on disk, then trace backwards to where the destination name comes from. The grep below gets me the candidates; the real work is reading each one. For every write I ask a single question, and it is the same question that would have caught CVE-2026-85097 in review: does the property you validated decide the property that gets executed? If the bytes are checked but a separate, client-controlled field names the file, the check is decoration.

# Smell to hunt in any upload handler:
# the stored filename, extension or directory derived from request input.
rg -n "basename\(|get_param|\$_POST|\$_REQUEST|json_decode" --glob '*.php'

# Then read each write sink by hand: move_uploaded_file, copy, file_put_contents, rename.
# The one question that confirms the bug:
#   does the property you VALIDATED (the bytes, the MIME) decide the property
#   that gets EXECUTED (the extension, the directory)?
# If a separate, client-controlled field names the sink, the check is decoration.

Is this your fire? Only if you run Bricksforge — but then, yes

Be honest about scale. Bricksforge has around 6,000 active installations by Patchstack’s count, so this is not an internet-wide event. [1] If you don’t run Bricksforge, you can close this tab. If you do, the window is not theoretical. Patchstack logged exploitation from 63 unique source IPs starting within hours of disclosure, with 95% of the malicious requests disguising a PHP payload as a GIF or PNG, and small clusters cycling through dozens of extension spellings to beat naive filters. [1]

The evasion is worth seeing, because it tells you what to grep for. Attackers tried alternate PHP extensions like .php7 and .phtml, case tricks like .PHp, and URL-encoded forms like %2ephp, all aimed at the same copy(). [1] The files they tried to drop followed patterns like login_admin_*.php in the uploads directory, including the Bricksforge temp folder. [1] As of 12 October the CVE is not yet in CISA’s Known Exploited Vulnerabilities catalog and its EPSS score is under one percent, but neither means quiet. [2] It means early. KEV is a trailing indicator; honeypots are a leading one, and the honeypots are already loud.

So the three conditions for this to be your emergency: you run Bricksforge, on a version below 3.1.8.10, reachable from the internet. If all three hold, patching is step one, not the finish line, because a PHP file that could be written may already have been written and run. For a site owner the question is not only “did we update” but “did anyone reach the form_submit endpoint with a file payload before we did”. That answer lives in your logs and your uploads directory, not in your plugin screen.

What to do before you close this tab

Here is the order I’d work in. Patch first, because the exploitation window is measured in hours, not weeks. [1] But if a vulnerable version faced the internet, assume someone tried, and hunt before you declare victory. A patch stops the next upload; it does nothing about a shell that was planted last week.

The list below is concrete. The log signature to look for is the one the attack can’t avoid: a request to the form submission endpoint carrying a temporaryFileUploads body where the file path points at an image but the url ends in a PHP extension. That mismatch is the bug’s fingerprint, and it is hard to fake innocently.

Patch and verify (in this order):
  [ ] Find every site running Bricksforge; confirm the version actually LOADED.
  [ ] Update to 3.1.8.10+ (3.x branch) or 4.0.1+. Bricksforge is license-gated and
      not on WordPress.org, so a lapsed license can silently block the update.
  [ ] Cannot update now? Block at the WAF / server: POST to
      /wp-json/bricksforge/v1/form_submit and the admin-ajax action
      bricksforge_form_submit carrying a temporaryFileUploads body.

If a vulnerable version faced the internet, assume compromise and hunt:
  [ ] Grep uploads for unexpected .php, especially
      /wp-content/uploads/bricksforge/tmp/ and names like login_admin_*.php.
  [ ] Audit for hidden admin accounts by querying the database directly,
      and review the must-use (mu-plugins) directory.
  [ ] In access logs, look for form_submit requests whose file path is an image
      but whose url field ends in a PHP extension.
  [ ] Rotate secrets reachable from PHP: database, API keys, SMTP.

The part “we validate uploads” never covers

CVE-2026-85097 is a tidy lesson in why “we validate uploads” is not an answer to “are uploads safe”. The plugin validated the one thing that was easy to validate and trusted the one thing that mattered. That gap, between the property you check and the property that executes, is where a surprising number of upload bugs live, and you can’t see it from a version number or a scanner’s green tick. You have to read the path from the request to the write.

Tracing that path, from an anonymous request through the framework to the exact line that names a file on disk, is the core of a web application assessment, and the “validate one thing, act on another” smell in this CVE is one I look for in code that has nothing to do with WordPress. If you are running forms, uploads or any flow that turns outside input into files on your server, that is the boundary I’d test for you. Tell me what you are shipping and we’ll scope it.

Sources

Need this checked in your own product?

Web & API penetration testing
All articles

08Contact

Shipping something that handles user data?

Send me a short brief. I reply within three days with questions or a proposal outline, and the price is agreed in writing before work starts.