The First Cookie Wins When Conflicting Cookie Names Are Used With Different Settings In Lucee CFML 5.3.6.61
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
andmycookie
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 thecookie
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:
Now, if we go over to our "read" page, here's what ColdFusion exposes on the cookie
scope:
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:
Now, if we go over to our "read" page, here's what ColdFusion exposes on the cookie
scope:
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
There was some discussion about adding a buffer for all headers within Lucee, rather than always just passing them to the servlet API
https://luceeserver.atlassian.net/browse/LDEV-1747
https://dev.lucee.org/t/setclientcookies-false-per-request/3631/7
@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).
@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 rawCookie
header. TheCookie
header contains all of the cookie values, but as a single String which we can parse.