Nov 21, 2010

Strict-Transport-Security in Struts 2

One of the topics of the upcoming OWASP Global Summit is Browser Security and the new security features in the form of optional HTTP headers.
Is this the path towards enduser security in the era of web applications? Perhaps. Anyway, I did some proof-of-concept implementations to check out how they work and which browsers support them. In this blog post I'll cover the first one ...

HTTP Strict-Transport-Security
All the details are in the draft specification so I won't spend too much time explaining it here. Basically, it's about a response header like this:

Strict-Transport-Security: max-age=60; includeSubDomains

... where the max-age is specified in seconds and the includeSubDomains directive is optional. The header tells the browser to only accept or set up HTTPS connections with that domain for a number of seconds ahead. Further, the browser should not accept any kind of shortcomings of the SSL certificate presented by the server and should not let the user click through.

Strict-Transport-Security as a Struts 2 Interceptor
I implemented this as a Struts 2 interceptor:

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.StrutsStatics;

import javax.servlet.http.HttpServletResponse;

public class StrictTransportSecurityInterceptor extends AbstractInterceptor {
    private static final Log logger = LogFactory.getLog(StrictTransportSecurityInterceptor.class);
    private static final String HSTS_HEADER = "Strict-Transport-Security";
    private static final String HSTS_VALUE_NAME = "max-age=";
    private static final int HSTS_VALUE_IN_SECONDS = 10;
    private static final String HSTS_VALUE_INCLUDE_SUBDOMAINS = "; includeSubDomains";

    public String intercept(ActionInvocation invocation) throws Exception {
        ActionContext context = invocation.getInvocationContext();
        HttpServletResponse response = (HttpServletResponse) context.get(StrutsStatics.HTTP_RESPONSE);
        String headerValue = HSTS_VALUE_NAME + HSTS_VALUE_IN_SECONDS;
        response.addHeader(HSTS_HEADER, headerValue);
        logger.debug("HSTS interceptor with policy: " + headerValue);
        return invocation.invoke();

And the interceptor can be used either directly (in struts.xml):

<package extends="struts-default" name="secureApp">
   <interceptor class="se.johnwilander.secureApp.strutsInterceptors.StrictTransportSecurityInterceptor" name="strictTransportSecurityInterceptor">

  <action class="se.johnwilander.secureApp.strutsActions.RegisterAction" name="register">
    <interceptor-ref name="strictTransportSecurityInterceptor"></interceptor-ref>
    <result name="success">/index.jsp</result>

... or be included in your custom Struts 2 interceptor stack.

Supported in Chrome 7, FF 3 + NoScript, and forthcoming FF 4
HSTS is only supported in Mozilla's and Google's browsers at the moment. But given the amount of attention around SSL problems and session hijacking lately (SSLStrip, Firesheep) I think Apple, Opera, and Microsoft will follow soon.

HSTS Draft Spec Doesn't Cover Non-Default Ports
When I tested the above interceptor on my local setup I could only get it to work in Firefox + NoScript, not in Chrome or Minefield (Firefox 4 beta). After contacting Mozilla (and Google) they explained that the specification is unclear on what the browser should do with non-default ports such as 8080 and 8443. Here's what Sid Stamm at Mozilla told me:

Hi John,

Basically, HSTS is not specified to do anything with non-default ports.

With regards to the Minefield implementation, kind of explains that non-default port handling is not addressed very well in the HSTS specification.  Non-standard ports are not changed by the "upgrade" performed by HSTS, but port 80 is changed to 443 (because 80 is default for HTTP and 443 is default for HTTPS).

The main use case that triggered the development of HSTS is that users don't usually type the scheme or port in address bar.  My reasoning for implementing it to ignore non-default ports is as follows:

  • If a user requests (by typing in address bar, following link or bookmark, etc) a specific port, they should get that port.
  • If the user doesn't type a port, they get the default port. In the case where no port _or_ scheme is typed, they currently get http on port 80 (which for HSTS hosts, is "upgraded").
  • If the scheme https is entered, HSTS is not needed.

I hope this helps. Basically, what I'm saying is that the behavior you noticed is intended. If you change the http server port to 80 and the https port to 443, HSTS should work as specified.

This of course makes it a little bit harder to test on your own machine. You'll have to set up some kind of  forwarding of port 80 to 8080 and 443 to 8443 so that the browser detects the switch from default https to default http.

MItM Attacks Possible By Abusing Non-Default Ports?
Making HSTS testing harder on your localhost is a minor problem compared to the possibility of circumventing the whole protection scheme by using non-default ports. Today SSLStrip just changes from default 443 to default 80 when it strips all https links, but it should be perfectly possible to change all those links to 8080 or the like. Client firewalls might refuse the request but I doubt it in the general case.

I would rather like HSTS to be effective for all port configurations but allow the enduser to configure it under the browser's preferences menu.


  1. We're working on addressing this in the spec. I think the expectation is that TLS will always be used for all ports for the host that advertises an HSTS policy. You should not be able to defeat HSTS with non-default ports.

  2. Oh, btw, Chrome does force upgrade to TLS on every port.

  3. @Security Retentive: I did the same tests with Chrome 7 and could not get it to enforce HSTS. They have not yet responded to my email though.

    As always, the devil is in the details so I might be getting things wrong, but the very same configuration provoked HSTS enforcement with Firefox 3.6.12 + NoScript.

  4. The "correct" behavior we've all discussed is:

    1. Convert HTTP to HTTPS
    2. If the port is 80, change the port to 443
    3. If the port is not 80, then force-TLS, but do not mangle ports

    I was pretty sure that was what Sid and Adam (L) implemented. I can check with them. Not sure why the problem.

  5. @Security Retentive: That's the kind of behavior I would expect.

    Mozilla posted a bug report:

    We'll see if they get the same results I do.

  6. OK, I read your bug report. The spec isn't especially clear. In HTTP/HTTPS land, and how browsers treat URLs/URIs, only port 80 and 443 are given "special" treatment. They don't require explicit port references in the URI itself, and they match to a well known port.

    Because of they, HSTS can transparently convert HTTP to HTTPS, and automatically switch from the well-known port-80 to 443 when enforcing TLS.

    When the URI has a non-standard port, there are only two "reasonable" behaviors:

    1. Don't force TLS, and access the port with HTTP
    2. Use the specified port in the URI, but force-TLS on that socket connection

    An unreasonable behavior would be:
    3. Use some random port calculation scheme and convert the chosen port to another port via some non-standard port number manipulation/rewrite scheme.

    We chose #2 as the desired behavior, because #1 leaks cookies which we specifically don't want to do, and #3 is right out because there is no well-known way to calculate the TLS port that corresponds to any other random "high port".

  7. Who are you emailing about HSTS at Google? If it's not Dr Barth or myself (agl @ chromium dot org) then you're emailing the wrong people.