Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Jim Thomson
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Jim Thomson

Enforcing The HTTP Request Method In ColdFusion

By on
Tags:

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:

Form submissing being processed via POST 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":

Form submissing being processed via both GET and POST methods.

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:

Form submissing being processed via POST and throwing an error on GET.

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

81 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.)

isFormPost = CGI.REQUEST_METHOD eq "post";
isValidFormPost = (isFormPost 
    && structkeyexists(form, "email")
    && isvalid("email", form.email));
15,688 Comments

@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 a throw() like I did my example).

81 Comments

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).

81 Comments

We do use cfparam to predefine all expected form parameters, but only after the initial request has met the requirements of the structkeyexists() validation.

426 Comments

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.

426 Comments

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. 🙂

15,688 Comments

@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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel