The upload was an image. The check passed. Later, the same contents became executable PHP.
That is the detail worth keeping from the Bricksforge story back in the news today. It is tempting to file an upload vulnerability under missing validation and move on. This one had validation. The next step stopped respecting what it established.
I care about that distinction because it changes what I would look for in a review. Finding a MIME check in an upload handler answers one question. Following the file until the application uses it answers the one that matters.
Where the checked image became PHP
Patchstack's report, published on 8 October, describes CVE-2026-85097. An unauthenticated attacker could obtain a nonce, upload a GIF containing PHP code, and submit a form with altered metadata.
The file passed the initial MIME check. During submission, the file field referred to that image, while the client-controlled url field named a PHP destination. Bricksforge trusted the metadata and placed the contents there. Requesting that PHP file then executed the embedded code.
The same bytes went through two different interpretations. The upload check recognised an image. The eventual destination let the server treat its contents as a program.
Here is the failure compressed to the two fields that matter. This is a schematic, not a complete request:
file: /uploads/tmp/image.gif
url: https://example.com/uploads/image.phpThe dangerous decision happened at the handoff: information sent back by the browser determined where the server put the file.
A check has a scope
An image check is useful. It can reject input your application does not support, catch mistakes, and contribute to a layered upload policy. I would keep it.
But what exactly did it prove?
It established something about the uploaded contents at that point in the workflow. It did not authorise a later filename, approve a destination, or bind a subsequent form submission to the server's original record of the upload.
Those are separate decisions. Each needs its own source of authority.
The mistake is assuming a validated file stays safe through every later step. A file may acquire a different extension. A client may return different metadata. A later handler may interpret a field as an instruction that the first handler treated as display data.
In a review, I want to find where that interpretation changes. Who supplied the value? Who can replace it? What decision does it control now?
That is also why a screenshot of a tidy upload widget tells you little about the server behind it. The Bricksforge changelog for 4.0.1, dated 8 October, says the issue affected every site with Pro Forms active, including forms with no upload fields. The visible form was a poor way to judge exposure.
Read across the handlers
Reviewing the upload function on its own encourages a comforting little inventory: size limit, type check, temporary directory. Everything looks sensible.
Then you open the submission handler. It has a file reference, a URL, some metadata. Those names look reasonable too. The problem appears when you trace which values survive between them and which ones the client can replace.
This is the kind of reading I want developers to practise. In my routine for reading unfamiliar code, the useful exercise is predicting what the code will actually do. For an upload pipeline, extend that prediction across the request boundary.
Write down the final path before running anything. Explain where every part of it came from. If the answer includes a client field, ask why that field gets to make a storage decision.
An agent can help find the callers and follow the data. The judgement still has to be yours. Ask it to trace each value into the final write, then check the trace against the code.
If you stop at a fluent explanation of the upload validator, the rest of the workflow stays unread. That is adopting code without understanding its decisions.
I have no evidence that AI wrote the vulnerable Bricksforge code. This is an ordinary software boundary that deserves a better review.
Keep the storage decision on the server
OWASP's file-upload guidance recommends application-generated filenames, restricted allowed types, and storage outside the webroot or on a separate host. It also warns that a file-signature check cannot carry the defence alone.
For a two-step upload flow, my design would keep the server's record authoritative throughout. The first request creates a record for the stored object. The later request supplies an opaque upload ID. The server looks up the path, permitted type, and ownership or submission binding itself.
An opaque ID still needs authorisation. A random-looking identifier is no reason to let one submission claim another submission's upload.
The original filename can remain as display information. It should not become a filesystem path simply because the browser sent it back. Likewise, a public download URL should be derived from the stored object, rather than accepted as instructions for where to write it.
I would also keep uploaded content out of executable locations. Where the application must serve it publicly, the server configuration needs to treat it as data, with no PHP handler for that storage area. That gives you another boundary if an application-level check fails.
These are design recommendations, not a description of Bricksforge's patch. The vendor's changelog confirms the fix without documenting its implementation.
Test the journey the attacker takes
A unit test showing that the MIME validator accepts a GIF can be correct while the complete system remains vulnerable. The useful regression test continues past that point.
Start with a permitted upload. Complete the later submission with metadata that disagrees with the original object. Then inspect what was actually stored and served.
I would cover these cases:
- Changed destination. Submit an executable extension or a path outside the intended storage area. The client must never choose the final write location.
- Changed reference. Try another submission's upload ID, or reuse one after it has been consumed. Check the ownership and lifecycle rules your application actually promises.
- Unexpected upload data. Send upload metadata to a form that has no upload field. The backend should enforce its configured fields rather than trust the browser's form layout.
- Execution at retrieval. Request the stored file through the deployed serving configuration. Verify that uploaded contents cannot run as server-side code.
Those tests check behaviour across upload, submission, and retrieval. They make the security property explicit: client-supplied metadata cannot turn an accepted upload into an executable file.
They belong in an isolated test environment, with fixtures you control. You do not need to reproduce an attack against somebody else's site to check your own boundary.
The patch closes the entry point
Patchstack identifies 3.1.8.10 as the fixed version for the reported 3.x range. The vendor lists the Pro Forms upload fix in 4.0.1. Check the release line you operate rather than treating a larger version number as proof that a fix is included.
Patchstack also reports exploitation attempts beginning on 7 October at 21:47 UTC. On a site that was exposed, it recommends checking historical logs and the host for unexpected PHP files or unauthorised changes.
Patching and checking for an earlier intrusion answer different questions. Closing the upload route says nothing about a file an attacker already left behind.
The part I would take back to my own codebase is the handoff. Find the next handler after the check. Trace the values that determine where the file is written, then check how the server serves it.
The validator did its job. The workflow needed one too.