Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Eymard Ventura
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Eymard Ventura

The First Cookie Wins When Conflicting Cookie Names Are Used With Different Settings In Lucee CFML 5.3.6.61

By
Published in Comments (3)

A few weeks ago at InVision, we did one of the most dangerous things you can do in a production application: we messed with some Cookie settings. And, unfortunately, we got a little burned when the change produced some unexpected behaviors - unexpected in so much as we didn't have the best mental model for how the cookies would react. As such, I wanted to take a moment to look at how cookies behave when the ColdFusion server ends up creating conflicting cookie names with different domain settings in Lucee CFML 5.3.6.61.

When you set a cookie, you can specify the "domain" for which the cookie is active. Meaning, you can specify the range of host-names under which the cookie will be sent back to the server on subsequent requests. In our particular case, we were trying to migrate a cookie from a less specific domain to a more specific domain. For the sake of this exploration, let's say that we were trying to move the cookie:

  • From: .local.invisionapp.com

  • To: .projects.local.invisionapp.com

Notice that the subdomain is being switched from the more general .local to the more specific .projects.local.

Now, when you set a cookie with a given name, it's not quite like overriding the entry in a Map / Hash / Struct - it doesn't just clobber the previous name-value pair. Instead, each cookie is sent as an individual Set-Cookie HTTP Header; and the browser has to figure out how to coalesce all the Set-Cookie headers into a single document.cookie string.

This means that, in the case of switching domains, both cookies are stored on the client-side. And, both cookies are sent back to the ColdFusion server. On the server-side, ColdFusion must then coalesce the multiple cookie values into a single Struct; which means, only one of those cookies will "win".

ASIDE: To make things even more exciting, cookie names are case-sensitive. Which means, on the JavaScript side, MyCookie and mycookie are both stored and sent back to the server. Of course, since ColdFusion is case-insensitive, only one of those cookies will be accessible on the cookie scope.

To explore this, I created a small ColdFusion page that sets a cookie named benben. Depending on a URL parameter, I'm defining the cookie under one of the two domain names discussed above:

<cfscript>

	param name="url.type" type="string" default="full";

	if ( url.type == "full" ) {

		cookie[ "benben" ] = {
			value: "woot woot (for .projects.local.invisionapp.com)",
			domain: ".projects.local.invisionapp.com",
			secure: true,
			preserveCase: true,
			expires: "never"
		};

	} else {

		cookie[ "benben" ] = {
			value: "woot woot (for .local.invisionapp.com)",
			domain: ".local.invisionapp.com",
			secure: true,
			preserveCase: true,
			expires: "never"
		};

	}

</cfscript>

<!---
	After we SET the cookies on the ColdFusion Response, let's see how they present on
	the Client-side in JavaScript.
--->
<script type="text/javascript">

	for ( var cookie of document.cookie.split( /;\s*/g ) ) {

		var pair = cookie.split( /=/ );

		if ( pair[ 0 ] === "benben" ) {

			console.log( pair[ 0 ], ":", decodeURIComponent( pair[ 1 ] ) );

		}

	}

</script>

Then, I created another ColdFusion page that simply dumps-out the cookie scope on the Lucee CFML server:

<cfscript>
	
	dump( cookie );

</cfscript>

Easy peasy, lemon squeezy! And, now that we have a means to create cookies under different domain names, let's see what happens when we define the benben cookie twice using different settings.

In the first demo, I'm going to call the create.cfm page in the following order:

  • ./create.cfm?type=full
  • ./create.cfm?type=wildcard

This will define two Set-Cookie headers across two requests, with the more-specific host name first. And, when we do this, our "create" page reads as such:

Setting two cookies presents the first cookie first in JavaScript and document.cookie.

Now, if we go over to our "read" page, here's what ColdFusion exposes on the cookie scope:

Setting two cookies presents the first cookie first in Lucee CFML.

As you can see, on both the client-side and the server-side, the first cookie with the more-specific domain is shown.

Now, let's clear the cookies in the browser and try to call the "create" page with the following parameters:

  • ./create.cfm?type=wildcard
  • ./create.cfm?type=full

This time, we're going to define two Set-Cookie headers across two requests, with the less-specific host name first. And, when we do this, our "create" page reads as such:

Setting two cookies presents the first cookie first in JavaScript and document.cookie.

Now, if we go over to our "read" page, here's what ColdFusion exposes on the cookie scope:

Setting two cookies presents the first cookie first in Lucee CFML.

As you can see, on both the client-side and the server-side, the first cookie with the less-specific domain is shown.

What we can see here is that nothing "clever" is happening on either the JavaScript / client-side or the Lucee CFML / server-side of the network. In both cases, the first cookie wins.

This is important to understand because it means that attempting to change the configuration of cookie that's already "out in the wild" isn't going to have the effect that you might expect it to. Both the browser and the server are going to continue seeing the older cookie (assuming it hasn't expired).

In hindsight, the better way to have handled our migration may have been to expire the old-cookie and the same time we were setting the new-cookie. We can do this by setting the expires attribute to NOW on the old-cookie domain:

<cfscript>

	cookie[ "benben" ] = {
		value: "",
		domain: ".local.invisionapp.com",
		secure: true,
		preserveCase: true,
		expires: "now" // Setting the OLD cookie, at less-specific domain, to expire.
	};

	cookie[ "benben" ] = {
		value: "woot woot (for .projects.local.invisionapp.com)",
		domain: ".projects.local.invisionapp.com",
		secure: true,
		preserveCase: true,
		expires: "never"
	};

</cfscript>

When doing this, the ColdFusion server sends back two Set-Cookie HTTP headers, one telling the old cookie to expire; and one defining the new cookie under the more-specific domain setting.

Cookies power the stateful web. And yet, despite their critical role in my day-to-day life, I've never had a good mental model for how they work. At least now I know that when you try to define two cookies with conflicting names, the first cookie wins in both JavaScript and Lucee CFML 5.3.6.61.

Want to use code from this post? Check out the license.

Reader Comments

15,902 Comments

@Zac,

Oh, interesting. I'm not entirely sure this would address our particular issue at work since we were only setting a single cookie on any given request; but, we had previously set other cookies with the same name (but with different settings).

15,902 Comments

@All,

A quick follow-up post to see if I could still access all of the original cookies even if the cookie scope only shows me the first one sent over the wire:

www.bennadel.com/blog/3893-accessing-cookies-with-the-same-name-in-lucee-cfml-5-3-6-61.htm

The getHttpRequestData() is our friend in this case, since it exposes the HTTP Headers, which gives us access to the raw Cookie header. The Cookie header contains all of the cookie values, but as a single String which we can parse.

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