Index: connectors/util/java/org/apache/tomcat/util/http/ServerCookie.java =================================================================== --- connectors/util/java/org/apache/tomcat/util/http/ServerCookie.java (revision 759131) +++ connectors/util/java/org/apache/tomcat/util/http/ServerCookie.java (working copy) @@ -18,11 +18,14 @@ package org.apache.tomcat.util.http; import java.io.Serializable; +import java.text.DateFormat; import java.text.FieldPosition; +import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; import org.apache.tomcat.util.buf.ByteChunk; -import org.apache.tomcat.util.buf.DateTool; import org.apache.tomcat.util.buf.MessageBytes; @@ -50,6 +53,37 @@ private int maxAge = -1; private int version = 0; + // Other fields + private static final String OLD_COOKIE_PATTERN = + "EEE, dd-MMM-yyyy HH:mm:ss z"; + private static final ThreadLocal OLD_COOKIE_FORMAT = + new ThreadLocal() { + protected DateFormat initialValue() { + DateFormat df = + new SimpleDateFormat(OLD_COOKIE_PATTERN, Locale.US); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + return df; + } + }; + private static final String ancientDate; + + + static { + ancientDate = OLD_COOKIE_FORMAT.get().format(new Date(10000)); + } + + /** + * If set to true, we parse cookies according to the servlet spec, + */ + public static final boolean STRICT_SERVLET_COMPLIANCE = + Boolean.valueOf(System.getProperty("org.apache.catalina.STRICT_SERVLET_COMPLIANCE", "false")).booleanValue(); + + /** + * If set to false, we don't use the IE6/7 Max-Age/Expires work around + */ + public static final boolean ALWAYS_ADD_EXPIRES = + Boolean.valueOf(System.getProperty("org.apache.tomcat.util.http.ServerCookie.ALWAYS_ADD_EXPIRES", "true")).booleanValue(); + // Note: Servlet Spec =< 2.5 only refers to Netscape and RFC2109, // not RFC2965 @@ -127,6 +161,7 @@ private static final String tspecials = ",; "; private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t"; + private static final String tspecials2NoSlash = "()<>@,;:\\\"[]?={} \t"; /* * Tests a string and returns true if the string counts as a @@ -139,6 +174,11 @@ * if it is not */ public static boolean isToken(String value) { + return isToken(value,null); + } + + public static boolean isToken(String value, String literals) { + String tspecials = (literals==null?ServerCookie.tspecials:literals); if( value==null) return true; int len = value.length(); @@ -164,9 +204,13 @@ } return false; } - - + public static boolean isToken2(String value) { + return isToken2(value,null); + } + + public static boolean isToken2(String value, String literals) { + String tspecials2 = (literals==null?ServerCookie.tspecials2:literals); if( value==null) return true; int len = value.length(); @@ -230,9 +274,6 @@ } } - private static final String ancientDate = - DateTool.formatOldCookie(new Date(10000)); - // TODO RFC2965 fields also need to be passed public static void appendCookieValue( StringBuffer headerBuf, int version, @@ -242,7 +283,8 @@ String domain, String comment, int maxAge, - boolean isSecure ) + boolean isSecure, + boolean isHttpOnly) { StringBuffer buf = new StringBuffer(); // Servlet implementation checks name @@ -250,7 +292,7 @@ buf.append("="); // Servlet implementation does not check anything else - maybeQuote2(version, buf, value); + version = maybeQuote2(version, buf, value,true); // Add version 1 specific information if (version == 1) { @@ -273,28 +315,34 @@ // Max-Age=secs ... or use old "Expires" format // TODO RFC2965 Discard if (maxAge >= 0) { - if (version == 0) { + if (version > 0) { + buf.append ("; Max-Age="); + buf.append (maxAge); + } + // IE6, IE7 and possibly other browsers don't understand Max-Age. + // They do understand Expires, even with V1 cookies! + if (version == 0 || ALWAYS_ADD_EXPIRES) { // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format ) buf.append ("; Expires="); // To expire immediately we need to set the time in past if (maxAge == 0) buf.append( ancientDate ); else - DateTool.formatOldCookie - (new Date( System.currentTimeMillis() + - maxAge *1000L), buf, - new FieldPosition(0)); - - } else { - buf.append ("; Max-Age="); - buf.append (maxAge); + OLD_COOKIE_FORMAT.get().format( + new Date(System.currentTimeMillis() + + maxAge*1000L), + buf, new FieldPosition(0)); } } // Path=path if (path!=null) { buf.append ("; Path="); - maybeQuote2(version, buf, path); + if (version==0) { + maybeQuote2(version, buf, path); + } else { + maybeQuote2(version, buf, path, ServerCookie.tspecials2NoSlash, false); + } } // Secure @@ -302,6 +350,10 @@ buf.append ("; Secure"); } + // HttpOnly + if (isHttpOnly) { + buf.append("; HttpOnly"); + } headerBuf.append(buf); } @@ -332,27 +384,40 @@ * @param buf * @param value */ - public static void maybeQuote2(int version, StringBuffer buf, - String value) { + public static int maybeQuote2 (int version, StringBuffer buf, String value) { + return maybeQuote2(version,buf,value,false); + } + + public static int maybeQuote2 (int version, StringBuffer buf, String value, boolean allowVersionSwitch) { + return maybeQuote2(version,buf,value,null,allowVersionSwitch); + } + + public static int maybeQuote2 (int version, StringBuffer buf, String value, String literals, boolean allowVersionSwitch) { if (value==null || value.length()==0) { buf.append("\"\""); } else if (containsCTL(value,version)) throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value"); else if (alreadyQuoted(value)) { buf.append('"'); - buf.append(escapeDoubleQuotes(value,1,value.length()-1)); buf.append('"'); + buf.append(escapeDoubleQuotes(value,1,value.length()-1)); buf.append('"'); - } else if (version==0 && !isToken(value)) { + } else if (allowVersionSwitch && (!STRICT_SERVLET_COMPLIANCE) && version==0 && !isToken2(value, literals)) { buf.append('"'); buf.append(escapeDoubleQuotes(value,0,value.length())); buf.append('"'); - } else if (version==1 && !isToken2(value)) { + version = 1; + } else if (version==0 && !isToken(value,literals)) { buf.append('"'); buf.append(escapeDoubleQuotes(value,0,value.length())); buf.append('"'); + } else if (version==1 && !isToken2(value,literals)) { + buf.append('"'); + buf.append(escapeDoubleQuotes(value,0,value.length())); + buf.append('"'); } else { buf.append(value); } + return version; } /** Index: container/catalina/src/share/org/apache/catalina/Context.java =================================================================== --- container/catalina/src/share/org/apache/catalina/Context.java (revision 759131) +++ container/catalina/src/share/org/apache/catalina/Context.java (working copy) @@ -181,8 +181,24 @@ */ public void setCookies(boolean cookies); + /** + * Gets the value of the use HttpOnly cookies for session cookies flag. + * + * @return true if the HttpOnly flag should be set on session + * cookies + */ + public boolean getUseHttpOnly(); + /** + * Sets the use HttpOnly cookies for session cookies flag. + * + * @param useHttpOnly Set to true to use HttpOnly cookies + * for session cookies + */ + public void setUseHttpOnly(boolean useHttpOnly); + + /** * Return the "allow crossing servlet contexts" flag. */ public boolean getCrossContext(); Index: container/catalina/src/share/org/apache/catalina/connector/Request.java =================================================================== --- container/catalina/src/share/org/apache/catalina/connector/Request.java (revision 759131) +++ container/catalina/src/share/org/apache/catalina/connector/Request.java (working copy) @@ -2238,7 +2238,7 @@ Cookie cookie = new Cookie(Globals.SESSION_COOKIE_NAME, session.getIdInternal()); configureSessionCookie(cookie); - response.addCookie(cookie); + response.addCookieInternal(cookie, context.getUseHttpOnly()); } if (session != null) { Index: container/catalina/src/share/org/apache/catalina/connector/Response.java =================================================================== --- container/catalina/src/share/org/apache/catalina/connector/Response.java (revision 759131) +++ container/catalina/src/share/org/apache/catalina/connector/Response.java (working copy) @@ -932,7 +932,18 @@ * @param cookie Cookie to be added */ public void addCookie(final Cookie cookie) { + addCookieInternal(cookie, false); + } + /** + * Add the specified Cookie to those that will be included with + * this Response. + * + * @param cookie Cookie to be added + * @param httpOnly Should the httpOnly flag be set on this cookie + */ + public void addCookieInternal(final Cookie cookie, final boolean httpOnly) { + if (isCommitted()) return; @@ -950,7 +961,8 @@ (sb, cookie.getVersion(), cookie.getName(), cookie.getValue(), cookie.getPath(), cookie.getDomain(), cookie.getComment(), - cookie.getMaxAge(), cookie.getSecure()); + cookie.getMaxAge(), cookie.getSecure(), + httpOnly); return null; } }); @@ -958,7 +970,7 @@ ServerCookie.appendCookieValue (sb, cookie.getVersion(), cookie.getName(), cookie.getValue(), cookie.getPath(), cookie.getDomain(), cookie.getComment(), - cookie.getMaxAge(), cookie.getSecure()); + cookie.getMaxAge(), cookie.getSecure(), httpOnly); } // if we reached here, no exception, cookie is valid Index: container/catalina/src/share/org/apache/catalina/core/StandardContext.java =================================================================== --- container/catalina/src/share/org/apache/catalina/core/StandardContext.java (revision 759131) +++ container/catalina/src/share/org/apache/catalina/core/StandardContext.java (working copy) @@ -656,6 +656,10 @@ */ private boolean saveConfig = true; + /** + * The flag that indicates that session cookies should use HttpOnly + */ + private boolean useHttpOnly = false; // ----------------------------------------------------- Context Properties @@ -1045,9 +1049,36 @@ new Boolean(this.cookies)); } + + /** + * Gets the value of the use HttpOnly cookies for session cookies flag. + * + * @return true if the HttpOnly flag should be set on session + * cookies + */ + public boolean getUseHttpOnly() { + return useHttpOnly; + } /** + * Sets the use HttpOnly cookies for session cookies flag. + * + * @param useHttpOnly Set to true to use HttpOnly cookies + * for session cookies + */ + public void setUseHttpOnly(boolean useHttpOnly) { + boolean oldUseHttpOnly = this.useHttpOnly; + this.useHttpOnly = useHttpOnly; + support.firePropertyChange("useHttpOnly", + new Boolean(oldUseHttpOnly), + new Boolean(this.useHttpOnly)); + } + + + + + /** * Return the "allow crossing servlet contexts" flag. */ public boolean getCrossContext() { Index: container/webapps/docs/changelog.xml =================================================================== --- container/webapps/docs/changelog.xml (revision 759136) +++ container/webapps/docs/changelog.xml (working copy) @@ -54,6 +54,10 @@ 42419: Add a system property that enables the name of the session cookie and session path parameter to be configured. (markt) + + 44382: Add support for using httpOnly for session cookies. + This is disabled by default. (markt/fhanik) + 45576: JAAS Realm now works with DIGEST authentication. (markt) @@ -67,6 +71,10 @@ logging at the context level but the security policy prevents this. (markt/rjung) + + 46597: Port all cookie handling changes from Tomcat 6.0.x. + (markt) + Index: container/webapps/docs/config/context.xml =================================================================== --- container/webapps/docs/config/context.xml (revision 759131) +++ container/webapps/docs/config/context.xml (working copy) @@ -235,6 +235,13 @@ implementation class that will be used for servlets managed by this Context. If not specified, a standard default value will be used.

+ + +

Should the HttpOnly flag be set on session cookies to prevent client + side script from accessing the session ID? Defaults to + false.

+
+ Index: container/webapps/docs/config/systemprops.xml =================================================================== --- container/webapps/docs/config/systemprops.xml (revision 759131) +++ container/webapps/docs/config/systemprops.xml (working copy) @@ -101,17 +101,32 @@

If this is true the following actions will occur:

    -
  • any wrapped request or response object passed to an application - dispatcher will be checked to ensure that it has wrapped the original - request or response. (SRV.8.2 / SRV.14.2.5.1) -
  • -
  • when updating the access count for the session, the update will be - synchronized. -
  • +
  • any wrapped request or response object passed to an application + dispatcher will be checked to ensure that it has wrapped the original + request or response. (SRV.8.2 / SRV.14.2.5.1) +
  • +
  • when updating the access count for the session, the update will be + synchronized. +
  • +
  • + cookies will be parsed strictly, by default v0 cookies will not work + with any invalid characters.
    If set to false, any v0 cookie with + invalid character will be switched to a v1 cookie and the value will + be quoted. +

+ +

If this is true Tomcat will always add an expires + parameter to a SetCookie header even for cookies with version greater than + zero. This is to work around a known IE6 and IE7 bug that causes IE to + ignore the Max-Age parameter in a SetCookie header.If not specified, the + default value of true will be used.

+
+