Just lately, I have been seeing a couple of errors in my ColdFusion logs for malicious requests which might be making an attempt to publish file uploads with a kind area named, add[]. The [] notation is Lucee CFML’s manner of grouping like-named fields into an array (versus the standard comma-delimited listing). It is a implausible function; and for the previous few years in Adobe ColdFusion, I have been utilizing a polyfill to supply that very same habits in my purposes.
However I solely examined my polyfill with URL-encoded kind submissions. And it appears that evidently as soon as you alter the shape’s enctype to multipart/form-data, the polyfill breaks. To get round this, we are able to use an undocumented function of Adobe ColdFusion — the kind.getPartsArray() methodology.
I’ve by no means seen this methodology earlier than; and there is solely a handful of references to it on Google, none of which explains the place the writer study it:
Warning: the next paragraph is Claude Code’s finest guess as to what this methodology is and when it was launched to Adobe ColdFusion. I’ve not been capable of confirm these information.
Apparently, ColdFusion has been utilizing Jason Hunter‘s com.oreilly.servlet (COS) library and its MultipartParser since possibly ColdFusion MX (ie, ACF 6, some 20 years in the past). When a kind is submitted with multipart/form-data, this parser kicks in and exposes its outcomes on the kind.getPartsArray() methodology; which returns a set that accommodates a mix of “param” and “file” elements:
When there is not any kind submission, or when the shape has been submitted with URL-encoded parameters, the kind.getPartsArray() methodology returns null.
Since I’ve by no means used this undocumented methodology earlier than, I needed to place collectively a bit proof-of-concept (POC) earlier than I tried to weave it into my current polyfill. On this POC, I’ve three <kind> tags that every submit a unique kind of request:
methodology="GET"methodology="POST"methodology="POST"withenctype="multipart/form-data"
Every kind submits most of its information utilizing [] bracket notation to group like-named fields into an array. And every kind submission features a v (“model”), which let’s me isolate the completely different contexts for server-side processing. However let me present you the HTML earlier than we have a look at the ColdFusion processing:
<cfscript>
// ... in separate code block later ...
</cfscript>
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>ColdFusion's FORM.getPartsArray() Methodology</title>
<hyperlink rel="stylesheet" kind="textual content/css" href="https://www.bennadel.com/weblog/./kinds.css">
</head>
<physique>
<!--- V(1) - URL solely. --->
<!--- Warning: FORM[GET] will drop search parameters within the ACTION attribute. --->
<kind methodology="get" motion="./index.cfm">
<enter kind="hidden" identify="v" worth="1" />
<enter kind="hidden" identify="ids[]" worth="1" />
<enter kind="hidden" identify="ids[]" worth="2" />
<enter kind="hidden" identify="ids[]" worth="3" />
<button kind="submit">
URL ONLY
</button>
</kind>
<!--- V(2) - FORM + URL, url-encoded parameters. --->
<kind methodology="publish" motion="./index.cfm?v=2&ids[]=1&ids[]=2&ids[]=3">
<enter kind="hidden" identify="type[]" worth="4" />
<enter kind="hidden" identify="type[]" worth="5" />
<enter kind="hidden" identify="type[]" worth="6" />
<button kind="submit">
URL + FORM
</button>
</kind>
<!--- V(3) - FORM + URL, multipart-encoded parameters. --->
<kind
methodology="publish"
motion="./index.cfm?v=3&ids[]=1&ids[]=2&ids[]=3"
enctype="multipart/form-data">
<enter kind="hidden" identify="type[]" worth="4" />
<enter kind="hidden" identify="type[]" worth="5" />
<enter kind="hidden" identify="type[]" worth="6" />
<enter kind="file" identify="filename[]" />
<enter kind="file" identify="filename[]" />
<enter kind="file" identify="filename[]" />
<button kind="submit">
URL + FORM / Multipart
</button>
</kind>
<cfdump label="URL Scope" var="#url#" />
<cfdump label="FORM Scope" var="#kind#" />
</physique>
</html>
Observe that on the backside of the web page, I am dumping-out the url and kind scopes. These scopes are going to be modified by the ColdFusion server-side processing primarily based on the v worth that’s submitted. Let’s stroll by every v individually.
Processing methodology="GET"
My polyfill already works for GET requests, grouping like-named URL parameters collectively. However I am showcasing it right here with specific processing with the intention to create a sturdy exploration. Since this can be a POC, I am hard-coding all of the URL and FORM keys since I do know what’s been submitted.
<cfscript>
// GET request.
if ( url.v == 1 ) {
// Observe: Changing the underlying param map right into a case-insensitive struct.
rawUrlScope = {}
.append( getPageContext().getRequest().getParameterMap() )
;
url.ids = []
.append( rawUrlScope[ "ids[]" ], true )
;
url.delete( "ids[]" );
}
</cfscript>
<!--- ... remainder of the demo (seen above)... --->
If the v=1 kind is submitted, we get the next web page output:
As you possibly can see, we translated the ids[] kind fields right into a url.ids entry, as an array, much less the [] suffix.
Processing methodology="POST"
My polyfill already works for “regular” POST requests, grouping each like-named URL parameters and like-named FORM parameters collectively. However I am showcasing it right here with specific processing with the intention to create a sturdy exploration. Since this can be a POC, I am hard-coding all of the URL and FORM keys since I do know what’s been submitted.
Observe that in a POST, the supply of the url parameters modifications:
<cfscript>
// POST request (with URL parameters as properly).
if ( url.v == 2 ) {
// Warning: when the request is a POST, the uncooked URL scope comes out of a unique
// parameter map. In a POST, the earlier parameter map now accommodates the FORM.
// --
// Observe: Changing the underlying param map right into a case-insensitive struct.
rawUrlScope = {}
.append( getPageContext().getRequest().getRequest().getParameterMap() )
;
url.ids = []
.append( rawUrlScope[ "ids[]" ], true )
;
url.delete( "ids[]" );
// Observe: Changing the underlying param map right into a case-insensitive struct.
rawFormScope = {}
.append( getPageContext().getRequest().getParameterMap() )
;
kind.type = []
.append( rawFormScope[ "sort[]" ], true )
;
kind.delete( "type[]" );
// kind.delete( "fieldnames" );
}
</cfscript>
<!--- ... remainder of the demo (seen above)... --->
If the v=2 kind is submitted, we get the next web page output:
As you possibly can see, we translated the ids[] kind fields and the type[] kind fields right into a url.ids entry and a kind.type entry, every as an array, much less the [] suffix.
Processing methodology="POST" and enctype="multipart/form-data"
Okay, now the principle focus of the POC. When a kind is submitted with multipart/form-data encoding, we are able to nonetheless use the getParameterMap() methodology, on the web page context, to get the url parameters. However, the opposite methodology:
getPageContext().getRequest().getRequest()
… is now undefined. Which is the place the undocumented kind.getPartsArray() comes into play.
Earlier than we have a look at the code although, let’s speak about file uploads in ColdFusion. When a consumer selects a file and posts it to the ColdFusion server, the shape worth that exhibits up within the kind scope is the short-term file location at which ColdFusion has saved the uploaded file on the ColdFusion server. So once you — the ColdFusion developer — go to name one thing like fileUpload(), all you are actually doing is shifting the file from the short-term location over to the everlasting location.
To complicate this, once you name fileUpload(), you do not give ColdFusion the temp file path, you give it the kind area identify. Then the fileUpload() BIF (built-in perform) does some magic behind the scenes to introspect the shape area and, presumably, extract the temp file location and course of it.
If we begin utilizing bracket notation ([]) to group file kind fields collectively, ColdFusion nonetheless goes by the short-term file machinations; however, as a result of we’re in polyfill territory right here, ColdFusion combines these temp file areas right into a single comma-delimited listing. Which implies that utilizing [] notation mainly rules-out the usage of ColdFusion’s native fileUpload().
However, since we now know that fileUpload() is little greater than a fileMove() name with some extra safety checks (vis-a-vis mime-type restrictions), we are able to re-implement that logic in our personal code.
The leap of religion that now we have to make, although — as a result of all of that is undocumented — is that the order of the temp file paths within the mixed comma-delimited listing exhibits up in the identical order because the kind.getPartsArray() assortment. This then permits us to devour the comma-delimited listing of temp file paths, one by one, and map them on to the pre-ColdFusion-processing information supplied by the getPartsArray() methodology.
Hopefully this makes extra sense once we have a look at the ColdFusion code:
<cfscript>
// POST request with `multipart/form-data` (with URL parameters as properly).
if ( url.v == 3 ) {
// Warning: when the request is a POST AND WE'RE USING MULTIPART FORM-DATA, the
// uncooked URL scope comes out of the identical parameter map as a once we're utilizing a GET.
// As a result of consistency can be foolish!
// --
// Observe: Changing the underlying param map right into a case-insensitive struct.
rawUrlScope = {}
.append( getPageContext().getRequest().getParameterMap() )
;
url.ids = []
.append( rawUrlScope[ "ids[]" ], true )
;
url.delete( "ids[]" );
kind.type = [];
kind.filename = [];
// With file-parts, issues get difficult. The elements that we're are the
// underlying elements of the incoming servlet request. However, ColdFusion builders
// are accustomed to seeing TEMPORARY FILE PATHS within the file-upload kind fields
// (ie, the place ColdFusion has positioned the uploaded file, outdoors of the present
// utility and throughout the the listing). Which the underlying request would not
// learn about as a result of it prepares its information earlier than ColdFusion consumes it. As such,
// that is the one place the place now we have to look within the kind scope to see which
// `.tmp` recordsdata ColdFusion has made out there. After which map these again onto the
// array of fields. Which is additional difficult, as a result of ColdFusion solely contains paths
// for populated file kind fields, not empty file kind fields. Which implies we might
// have fewer TMP paths than filename[] elements. We subsequently must preserve observe of
// what number of file paths we have consumed as we assemble our information.
consumedFileCount = 0;
// Get the gathering of `com.oreilly.servlet.multipart.*` situations. That is an
// UNDOCUMENTED function. However apparently ColdFusion has been transport with Jason
// Hunter's com.oreilly.servlet (COS) library and its MultipartParser since possibly
// ColdFusion MX (ie, ACF 6, 20 years in the past). There's just a few references of this
// on Google. This returns UNDEFINED besides when `multipart/form-data` requests
// are made to the server.
for ( half in kind.getPartsArray() ) {
// Non-file values (ie, form-scoped easy values).
// --
// https://www.servlets.com/cos/javadoc/com/oreilly/servlet/multipart/ParamPart.htm
if ( half.isParam() ) {
key = half.getName().left( -2 ); // Take away the brackets (`[]`) from the important thing.
kind[ key ].append( half.getStringValue( "utf-8" ) );
}
// File-upload values (ie, form-scoped binary values).
// --
// https://www.servlets.com/cos/javadoc/com/oreilly/servlet/multipart/FilePart.html
if ( half.isFile() ) {
key = half.getName().left( -2 ); // Take away the brackets (`[]`) from the important thing.
// If the filename is empty, the consumer did not choose a file. Which implies
// that ColdFusion will not have a `.tmp` file for it. However we nonetheless must
// populate the array-based kind polyfill. For that, I will present
// a "null struct" with empty values.
if ( ! len( half.getFileName() ?? "" ) ) {
kind[ key ].append({
filepath: "",
filename: "",
contentType: "",
});
proceed;
}
kind[ key ].append({
filepath: kind[ "filename[]" ].listGetAt( ++consumedFileCount ),
filename: half.getFileName(),
// CAUTION: Don't belief this user-provided content-type. This might
// be altered in a malicious request. You'll need to validate the
// content-type on the server-side processing half.
contentType: half.getContentType(),
});
}
}
kind.delete( "type[]" );
kind.delete( "filename[]" );
// kind.delete( "fieldnames" );
// CAUTION: As soon as we begin utilizing `[]` notation to group FILE UPLOAD FIELDS, we're
// mainly opting out of with the ability to use ColdFusion's fileUpload() BIF (as far
// as I can inform). As such, now we have to devour the `.tmp` filepath instantly,
// shifting and renaming recordsdata with our personal logic.
for ( entry in kind.filename ) {
if ( ! entry.filepath.len() ) {
proceed;
}
// Do not belief the content-type supplied by the consumer - we have to confirm that
// now we have the anticipated varieties on the server.
swap ( fileGetMimeType( entry.filepath ) ) {
case "picture/jpeg":
case "picture/png":
// Observe: I am hard-coding `.png` of the sake of debugging.
fileMove(
supply = entry.filepath,
vacation spot = expandPath( "./uploads/#createUuid()#.png" )
);
break;
}
}
}
</cfscript>
<!--- ... remainder of the demo (seen above)... --->
This can be a lot of code. I’ve tried to remark it properly sufficient, so I will not reiterate what it is doing right here. I will solely present you that after I choose two recordsdata, leaving one kind area clean, after which submit v=3, we get the next output:
As you possibly can see, we translated the ids[], type[], and filename[] kind fields all into their array-based counterparts, much less the [] suffix. And since we have already forgone the usage of fileUpload(), I’ve determined to make the “file add end result” constructions a bit extra strong, utilizing a number of the information that kind.getPartsArray() has supplied.
Processing the add then turns into a matter of performing any desired safety test (reminiscent of mime-type validation) after which utilizing fileMove() to maneuver the file from the half.filepath short-term location into the everlasting location for my utility.
However Possibly Do not Use This
So now we have a path ahead for utilizing bracket notation [] for multipart form-data submissions. However, possibly do not use this? This is without doubt one of the most undocumented options I’ve ever seen in ColdFusion. It is in all probability finest simply to call file uploads fields uniquely. As in:
… and so forth. after which use older-school ColdFusion strategies for processing the shape submission.
The issue, although — as I am considering this by — is you could’t mix-and-match approaches. Which means, once you submit a kind as multipart/form-data, the shape fields are solely out there through the kind.getPartsArray() methodology — not on the web page context. So even in the event you give file fields distinctive names, you continue to cannot use bracket notation for non-file-fields because the polyfill not has entry to any of the shape fields on the web page context .getParameterMap() methodology.
So… possibly do use this. Fairly the head-scratcher!
Life would simply be higher if Adobe supported bracket notation on kind fields, AMIRIGHT?!

