Cookie Reserved Names- Who's to blame?
Posted by
Brad Wood
Jul 12, 2008 04:35:00 UTC
Teeps started off the fun last month with this blog post about countless errors in his logs about not being able to set cookies.
I get the errors too, and they usually look like this:
[code]07/11 15:58:19 error Cannot create cookie: domain = .notingdetails.com 07/11 16:19:01 error Cannot create cookie: domain = .notingdetails.com 07/11 16:39:12 error Cannot create cookie: domain = .notingdetails.com 07/11 16:45:05 error Cannot create cookie: domain = .notingdetails.com [/code]Jochem chimed in with an explanation last week explaining that clients (most likely poorly written bots) were mis-interpreting cookie headers. Consider the following headers which come back from the server to the client:
[code]HTTP/1.1 200 OK Date: Sat, 12 Jul 2008 01:36:02 GMT Server: Apache/2.2.4 (Linux/SUSE) Set-Cookie: cookie_name=cookie_value;domain=.notingdetails.com;path=/admin Set-Cookie: JSESSIONID=5c30ce51736762297c2f;path=/ Content-Language: en-US Content-Type: text/html; charset=UTF-8 Content-Length: 10903 [/code]This header sets two cookies. Once called "cookie_name", and another called "JSESSIONID". The former cookie specifies a domain and a path, the latter just a path. Certain bots don't obey RFC specs and try to send back a cookie called "domain" like my logs above show. As it turns out ColdFusion's error logs get an entry in them for EVERY cookie that is attempted to be set with the one of the following names (the actual list is slightly larger than Jochem's original one):
- Comment
- Discard
- Domain
- Expires
- Max-Age
- Pathv
- Secure
- Version
[code]-Xdebug -Xrunjdwp:transport=dt_socket,address=28000, server=y, suspend=n [/code]This tells the JVM to start in debug mode and listen on port 28000 for a debugger to connect. suspend=n tells the JVM NOT to suspend until the debugger connects. The port number is arbitrary, just make sure it's not already in use by something. Then I placed Eclipse in the Debug perspective, and chose Run > Debug... > Remote Java Application. I entered the address of my server for host and 28000 for the port number and then clicked Debug. This connected to my ColdFusion server's JVM and brought up all the threads in its existance. I created a test page with a large loop so the page would run long enough for me to catch it. Using MS Fiddler I created a request that tried to set several cookies with reserved names such as "expires", "path", and "secure". I then sent the request, tabbed over to SeeFusion to get the thread name handling my request, and tabbed back over to Eclipse and did a Ctrl-F to filter down that thread and pause it. Now that I had my thread trapped, and back out to the JRunRequestDispatcher.invoke() method by using the drop to frame feature of the debugger. (rewinds the stack as if you were going back in time.) I then carefully stepped through JRUN until I saw the errors appear in my logs. This is where they went in:
[code]Cookie.<init>(String,String) LinL 170 JRunCookieUtils.parseCookieString(String, Vector, Logger) line: 180 JRunCookieUtils.getCookies(Enumeration, Logger) line: 152 JRunCookieUtils.getCookies(Enumeration, Logger) line: 152 JRunRequest.getCookies() line: 335 ForwardRequest(HttpServletRequestWrapper).getCookies() line: 108 SessionService.getCookieSessionID(HttpServletRequest) line: 1066 ForwardRequest.getRequestedSessionId() line: 42 ForwardRequest.isRequestedSessionIdValid() line: 467 ForwardRequest.getSession(boolean) line: 332 ForwardRequest.create(HttpServletRequest, HttpServletResponse, WebApplication, String, String, String, String, String, MultiKeyContainer) line: 117 JRunRequestDispatcher.invoke(ServletConnection) line: 257 ...[/code]The log entry happened when JRUN tried to create an instance of the javax.servlet.http.Cookie class. That code isn't part of ColdFusion OR JRUN. That comes from Sun. Luckily, Java is open source now, so I pulled up the source code for the javax.servlet.http.Cookie class. There it was, plain as day in the constructor:
[code] if (!isToken(name) || name.equalsIgnoreCase("Comment") // rfc2019 || name.equalsIgnoreCase("Discard") // 2019++ || name.equalsIgnoreCase("Domain") || name.equalsIgnoreCase("Expires") // (old cookies) || name.equalsIgnoreCase("Max-Age") // rfc2019 || name.equalsIgnoreCase("Path") || name.equalsIgnoreCase("Secure") || name.equalsIgnoreCase("Version") || name.startsWith("$") ) { String errMsg = lStrings.getString("err.cookie_name_is_token"); Object [] errArgs = new Object [1]; errArgs[0] = name; errMsg = MessageFormat.format(errMsg, errArgs); throw new IllegalArgumentException (errMsg); } [/code]The Cookie constructor will throw an IllegalArgumentException if any of those conditions are met. $ is used for special information like $version. The isToken method looks like this:
[code] private static final String tspecials = ",; "; private boolean isToken(String value) { int len = value.length(); for (int i = 0; i < len; i++) { char c = value.charAt(i); if (c < 0x20 || c >= 0x7f || tspecials.indexOf(c) != -1) return false; } return true; } [/code]That basically means any of the letters in a token can't be an ASCII control character (like line feed or DEL) and any of the characters can't be a comma or semi-colon. Interestingly enough, this does NOT follow RFC 2068 (which disallows the following: ()<>@,;:\\\"/[]?={} \t) but there is a note in there about "full Netscape compatibility". So, unless Sun changes their code, the only thing JRUN can do is not log errors thrown when creating instances of the Cookie class. Here's the thing though, as Jochem pointed out, I'm not convinced that Sun's code is correct. They cite RFC 2019 in their code, but I spent several hours reading through the RFC today (freaking dry as a dessert) and I don't get it. Here are some highlights from the RFC I pulled out:
- "[Cookie] NAMEs that begin with $ are reserved for other uses and must not be used by applications."
- In section 4.1 it states that attribute values pairs where the attributes must be a token confirming to RFC 2068.
- RFC 2068 (http://www.faqs.org/rfcs/rfc2068.html) says a token is 1 or more characters that are anything BUT CTLs or tspecials.
CTLs =
tspecials = "(" | ")" | "<">" | "@" | "," | ";" | ":" | "\" | <"/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
SP = <US-ASCII SP, space (32)>
HT = <US-ASCII HT, horizontal-tab (9)> - Futhermore, " The NAME=VALUE attribute-value pair must come first in each cookie. "
- And, "When [the server] receives a Cookie header, the origin server should treat cookies with NAMEs whose prefix is $ specially, as an attribute for the adjacent . The value for such a NAME is to be interpreted as applying to the lexically (left-to-right) most recent cookie whose name does not have the $ prefix."
[code]Set-Cookie: domain=foobar;domain=.yoursever.com[/code]In this example, the name and value of the cookie is required to be first, so there should be no reason why the domain attribute would be confused with the name. Client to server
[code]Cookie: domain=foobar;$domain=.yourserver.com[/code]Here $domain refers to the proceeding cookie called "domain" just like the RFC says. Frankly I don't think Sun's code is correct. I wouldn't be surprised if they did this because of some browser bug back in the day, but it will still be lame. In the mean time I think the only option to get rid of our errors might be to look into an Apache module that rewrites headers like mod_headers.
Tags: ColdFusion, Java
Charlie Arehart
Good investigative work, Brad. Thanks for sharing it. Let's see if any others chime in with more ideas. It would be nice to find some solution--and not everyone's on Apache, even if mod_headers solved it for them. :-)
Brad Wood
I don't quite know enough about Java to answer this yet, but I wonder if it is possible to recompile the jrun.jar with a modified javax.servlet.http.Cookie class. JRUN is closed source, but the Cookie class is not. Frankly I don't know that most people would be willing to fiddle that much with their install due to the potential risk, but I'm just brainstorming. Of course, I'll point out I am in no mood to modify JRUN in a way that would violate the license!