Articles
tempCache UDF
Leverages cacheput/get to store temporary secrets; ie short-lived client variables-on-demand without having to enable client variables
The tempCache user-defined function (UDF) allows you to temporarily store data in the ColdFusion cache and retrieve it using a generated UUID. This is particularly useful for preserving data across HTTP redirects, with options for automatic expiration and single-use retrieval.
There's been many occasions where a user-specific payload has been generated (shopping cart, check out, config settings, processing results) and the user needs to be directed to a new destination with the data, but I want to avoid non-securely passing data as URL or form parameters or having to enable and/or leverage session variables.
We've encountered issues where the content could be blocked due to complex WAF rules that are beyond our editable control... especially if there's anything that resembles HTML or contains certain sequences. There's also abuse issues as automated software can scan the form, fuzz the parameters in order to blindly auto-post to the final script. We experienced this in the form of carding (aka credit card stuffing) on some non-profit donation forms.
EXAMPLE: The inclusion of this UDF into the workflow of online check-out form recently prevented over 1,000 carding attacks during a late night 2.5 hour window. The tempCache UDF was configured for "single use" and the abuser was under the false impression that they could automate reposting data to the final check-out URL with different cards to test which were active. Instead of enforcing a captcha or rate limiting, we've opted to poison their results by adding a varied response delays (to simulate the background payment gateway check) and return bank error messages indicating that the credit card number is invalid.
The most impactful workflow has been to:
- display a verification page
- Create an object with data unique to the order (IP, email, total amount) , temporarily cache it server-side and generate a token to add to the form
- Upon submission, and prior to performing any transaction, use the UUID to perform a look-up of cached data. If the look-up data doesn't exist or doesn't match the form/CGI data, reject the attempt.
- Added bonus: If no cached data exists, sleep for a second or two and then return a bogus "credit card is invalid" message.
We've also used this script on Contact & "Thank you" pages. On some older applications, the response is displayed on the same page without redirecting or using history.pushState(null, null, "/myUrl"); to prevent accidental POST resubmission, but some app-based browsers seem to be blindly retriggering the form post when reopening the app. We haven't been able to determine the actual cause, but capturing the response message, adding it to an object, caching and redirecting to a new page with the UUID to display the content has prevented the form report/replay issues from reoccurring.
Source Code
<cfscript>
public any function tempCache(any inputObject, numeric minutes=5, numeric maxMinutes=30, boolean singleUse=false, string cachePrefix="tempCache_") hint="Temporarily cache data for 1-2 minutes to allow 302 redirect to perform without passing potentially blocked HTML in a form post. (Pass object; returns UUID. Pass UUID, returns object.)" {
local.response = "";
local.minutes = (val(arguments.minutes) gt 0) ? val(arguments.minutes) : 5;
local.maxMinutes = abs(val(arguments.maxMinutes)) + local.minutes;
if (issimplevalue(arguments.inputObject) && isvalid("UUID", arguments.inputObject)){
local.response = cacheget("#arguments.cachePrefix##arguments.inputObject#");
if (isnull(local.response)){
local.response = {};
} else if (arguments.singleUse){
cacheremove("#arguments.cachePrefix##arguments.inputObject#");
}
} else {
local.response = createuuid();
cacheput("#arguments.cachePrefix##local.response#", arguments.inputObject, createtimespan(0, 0, local.maxMinutes, 0), createtimespan(0, 0, local.minutes, 0));
}
return local.response;
}
</cfscript>
Demo
<cfscript>
// process token (if form post and token exists)
if (CGI.REQUEST_METHOD eq "post" && structkeyexists(form, "token")){
writeoutput('<div class="messageBox"><b>Processing Token:</b> <tt>#encodeforhtml(form.token)#</tt></div>');
if (!isvalid("UUID", form.Token)){
writeoutput('<div class="warningBox">Invalid token</div>');
}
cacheData = tempCache(form.Token);
if (!structcount(cacheData)) {
writeoutput('<div class="warningBox">Cache key doesn''t exist</div>');
} else if (!cacheData.keyexists("ipAddress")) {
writeoutput('<div class="warningBox">Cache key exists, but "ipAddress" key is missing?</div>');
} else if (cacheData.ipAddress neq CGI.REMOTE_ADDR) {
writeoutput('<div class="warningBox">"ipAddress" key exists, but it not same as prior request.</div>');
} else {
writeoutput("<h2>Cached data is retrieved, now process the rest of the request and redirect to a 'Thank You' page.</h2>");
cf_dump(var=cacheData, label="DUMP: cacheData");
}
exit;
}
</cfscript>
<cfscript>
// create cached payload and retrieve token
tempConfig = [
"inputObject": [
"html": "<p>Content that may be flagged by WAF or sensitive and shouldn't be displayed in the HTML source.</p>",
"ipAddress": CGI.REMOTE_ADDR,
"timestamp": datetimeformat(now(), "iso"),
"arrayItem": [javacast("int", 1), tostring(1)]
],
"minutes": 1,
"maxMinutes": 1,
"singleUse": false
];
cacheToken = tempCache(argumentcollection=tempConfig);
</cfscript>
<cfoutput>
<p><b>A new "60 second" cache token has been generated:</b> <tt>#cacheToken#</tt></p>
<fieldset>
<legend>Form with token <tt>#cacheToken#</tt></legend>
<form action="" method="post">
<input type="hidden" name="token" value="#cacheToken#">
<!-- This information doesn't have to be passed in the form. It's been cached on the server and exchanged with a token.
<input type="text" name"="html" value="<p>Content that may be flagged by WAF or sensitive and shouldn't be displayed in the HTML source.</p>">
<input type="text" name"="ipAddress" value="#CGI.REMOTE_ADDR#">
<input type="text" name"="timestamp" value="#datetimeformat(now(), "iso")#">
-->
<p><i>Form fields for processing on the next page (response message, original IP, timestamp, array with explicit data types) are not passed as plain text in this form. A GUID is passed as a token in it's place and the values will be retrieved if they haven't expired or been already used (ie, singleUse).</i></p>
<button type="submit">Process Form Submission</button>
</form>
</fieldset>
</cfoutput>
<cf_dump var="#tempConfig#" label="DUMP: tempCache parameters (debugging)">