Enforcing The HTTP Request Method In ColdFusion
HTTP GET
requests should always be safe to execute because they are intended to read the state of the system in an idempotent fashion. Other HTTP methods (verbs)—such as POST
and DELETE
—which are intended to mutate the state of the system, can be dangerous and should be validated. Meaning, you never want to allow a GET
request to invoke a resource that merits a POST
method. I wanted to take a quick look at how you can enforce the HTTP request method in ColdFusion.
The easiest way to enforce the HTTP method in ColdFusion is to do so implicitly by relying on the URL
and FORM
scopes to deliver the different sets of data. By default, the URL
scope only contains the data present in the request URL. And, the FORM
scope only contains the data present in a form submission.
Which means, if you need to ensure that a request is processed as part of a form submission, check the form
scope—and only the form
scope—for that data.
To see what I mean, let's look at a simple ColdFusion page that contains both a <form>
element and an <a>
element which seeks to impersonate the form:
<cfscript>
// In this version, the SUBMITTED value is only observed in the FORM scope, which
// means that we can be confident that the request was submitted via an HTTP POST (if
// the value is true).
param name="form.submitted" type="boolean" default=false;
if ( form.submitted ) {
// ... processing form, mutating the system state...
}
</cfscript>
<cfoutput>
<cfif form.submitted>
<p>
<mark>Thank you for your submission</mark>!
</p>
</cfif>
<!--- REAL form submission. --->
<form method="post" action="test.cfm">
<input type="hidden" name="submitted" value="true" />
<button type="submit">
Submit Form
</button>
</form>
<!--- FAKE (potentially malicious) form submission. --->
<p>
<a href="test.cfm?submitted=true">Fake Submit</a>
</p>
</cfoutput>
As you can see, both the <form>
and the <a>
are attempting to "submit" to the URL, test.cfm?submitted=true
; but, when we go to process the request, we are only looking at the form
scope for the submitted
flag. As such, we are implicitly testing the HTTP method:
Note that the "Thank you" message is only rendered during a POST
request and not during the GET
request. By using the appropriate ColdFusion scope, we've implicitly validated the request method.
In many of the ColdFusion web frameworks, however, the URL
and FORM
scopes are combined into a single "request context". So, instead of explicitly referencing the URL
and FORM
scopes, you rely on a generated scope, such as rc
(for "request context").
Since this rc
scope is populated by both the URL
and FORM
scopes, it means that "post" data can easily be provided by a "get" request. What follows is the same ColdFusion code as above, only we're replacing form.
with rc.
:
<cfscript>
// Mocking out a common framework approach in which the FORM and URL scopes are
// combined into a single "request context" scope.
rc = structNew()
.append( url )
.append( form )
;
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// In this version, the SUBMITTED value is observed in the RC scope, which means that
// it can provided either through the URL or the FORM scope. As such, we CANNOT be
// confident about the current HTTP method.
param name="rc.submitted" type="boolean" default=false;
if ( rc.submitted ) {
// ... processing form, mutating the system state...
}
</cfscript>
<cfoutput>
<cfif rc.submitted>
<p>
<mark>Thank you for your submission</mark>!
</p>
</cfif>
<!--- REAL form submission. --->
<form method="post" action="test2.cfm">
<input type="hidden" name="submitted" value="true" />
<button type="submit">
Submit Form
</button>
</form>
<!--- FAKE (potentially malicious) form submission. --->
<p>
<a href="test2.cfm?submitted=true">Fake Submit</a>
</p>
</cfoutput>
Now that the rc
scope is pulling data from both the URL
and FORM
scopes, both methods can trigger a "form submission":
As you can see, when we switch to the unified rc
scope, we no longer get the implicit validation of the HTTP method. Which means, both the GET
and POST
requests can trigger the "form processing" workflow.
At this point, we have to move from an implicit validation to an explicit validation. Which means, actually asserting that the incoming request is using the expected HTTP method / verb. And, for that, we can look at the CGI
scope, which contains the current request method:
<cfscript>
// Mocking out a common framework approach in which the FORM and URL scopes are
// combined into a single "request context" scope.
rc = structNew()
.append( url )
.append( form )
;
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// In this version, the SUBMITTED value is observed in the RC scope, which means that
// it can provided either through the URL or the FORM scope. As such, we CANNOT be
// confident about the current HTTP method.
param name="rc.submitted" type="boolean" default=false;
if ( rc.submitted ) {
// Since the SUBMITTED value might be coming through via a MALICIOUS GET, we need
// to validate / assert that the request method is a POST. Since any non-POST is
// considered malicious in this context, we don't need to "recover gracefully" -
// protecting the system is the priority, the user experience (UX) is not.
assertHttpMethod( "POST" );
// ... processing form, mutating the system state...
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I assert that the current HTTP request is using the given method. If so, this
* function quietly returns; if not, an error is thrown in order to protect the system.
*/
public void function assertHttpMethod( required string method ) {
if ( cgi.request_method == method ) {
return;
}
throw(
type = "Forbidden.MaliciousRequest",
method = "Invalid HTTP method used in request.",
detail = "Expected method: [#method#], actual method: [#cgi.request_method#]."
);
}
</cfscript>
<cfoutput>
<cfif rc.submitted>
<p>
<mark>Thank you for your submission</mark>!
</p>
</cfif>
<!--- REAL form submission. --->
<form method="post" action="test3.cfm">
<input type="hidden" name="submitted" value="true" />
<button type="submit">
Submit Form
</button>
</form>
<!--- FAKE (potentially malicious) form submission. --->
<p>
<a href="test3.cfm?submitted=true">Fake Submit</a>
</p>
</cfoutput>
As you can see, this time, inside the form processing block, we examine the cgi.request_method
and make sure that it actually contains the HTTP method that we need. And, if it doesn't, we throw an error and halt the current request processing control flow:
The explicit check is more verbose; but, it's also more flexible. When we implicitly validate the request via the URL
and FORM
scopes, we can really only differentiate between the GET
and POST
methods. However, when we add an explicit HTTP request method check, we can start to differentiate between all the HTTP verbs (including PUT
, PATCH
, DELETE
, OPTIONS
, HEAD
, etc).
Aside: Throwing an error might seem a bit heavy-handed. But, keep in mind that we're validating against an unexpected / malicious invocation. Recovering "gracefully" isn't necessary as this pathway isn't something a normal user should ever experience.
An Eye on Security
It's worth quickly discussing why it is (or, can be) dangerous to allow GET
routing to invoke state mutation. While POST
and DELETE
requests typically involve the user explicitly initiating an action, GET
requests can be triggered "on the sly" using something like an <img>
source. Unvalidated HTTP methods allow a "bad actor" to craft GET
requests for an unsuspecting victim:
<img src="/some/malicious/request" />
While this src
attribute may not point to a valid image, it won't matter - the browser will still make the GET
request on behalf of the victim. And, if the target server allows this GET
request to mutate the system, the "bad actor" will be able to make malicious requests to the system by proxy.
Enforcing the HTTP method isn't a solve-all; but, it is one part in a multi-prong security effort. One-time use form tokens, XSRF (Cross-Site Request Forgery) tokens, Content Security Policy (CSP), and SameSite
cookie settings are all additional mechanics that can be added on top of the HTTP method validation to help create a secure ColdFusion application.
Want to use code from this post? Check out the license.
Reader Comments
We validate the request method & test for at least one of the required form variables. If this condition is not met, we re-display the form. (If a form post was used and not valid, we display a pretty inline error message.)
Something like this... (NOTE: This is generic.)
@James,
I think that works well, I use
cfparam
to set defaults for a lot stuff; so, I tend to have very few "key exists" checks. But, I think it makes sense to have a "post" check as a precursor to actually processing the form (as opposed to just having athrow()
like I did my example).There was a CF4 custom tag called CF_FormURL2Attributes that would copy all FORM & URL variables to a generic "attributes" scope. (I believe that it also accepted path-based values from SEO-friendly URLs.) I don't see this CFTag available anywhere online anymore. We used it back in '98 and still use something similar to it today (but with lots of extra sanitization & security checks).
We do use
cfparam
to predefine all expected form parameters, but only after the initial request has met the requirements of thestructkeyexists()
validation.Good grief.
I have been using FW1 [Sean Corfield's amazing light weight MVC framework] for many years and I always assumed that this kind of validation, had been taken care of, in the background.
But, clearly, I now have some updates to carry out. 🤩
Thanks for pointing this out.
Good
Just to let you know, I do most of my Blog reading, in the evening, on my iPhone 8. I am using the latest version of iOS.
When I type into your comment text field, the form is submitted, automatically, after I type a few characters.
Hence my weird Good comment.
I had to copy & paste this comment. 🙂
@Charles,
I was actually just about to ask you if you were doing this on a mobile device. I noticed this same thing the other day on my iPhone. It looks like Hotwire is submitting the form when it goes to render the preview (instead of just rendering it to another Div). I have to fix that. 😫 I think I have to figure out if I want to update the Hotwire (v8 just came out, I think). Or, if I should just rip it out and go back to vanilla JavaScript. I'm not sure it is adding my value for this particular blog.
re: FW/1, if you use route-mappings, you can prefix a route with the HTTP method. But, if someone goes directly to the subsystem, then I think that won't be enforced.
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →