--- java/org/apache/catalina/filters/HstsFilter.java (revision 0) +++ java/org/apache/catalina/filters/HstsFilter.java (working copy) @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.filters; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Implements HTTP Strict Transport Security (HSTS) according to RFC 6797. + *

+ * The filter assumes that: + *

+ *

Filter Configuration

+ *

Basic configuration to add ' + * Strict-Transport-Security: max-age=31536000 ; includeSubDomains + * ' to all responses sent over SSL/TLS

+ * <web-app ...>
+ *    ...
+ *    <filter>
+ *       <filter-name>HstsFilter</filter-name>
+ *       <filter-class>org.apache.catalina.filters.HstsFilter</filter-class>
+ *       <init-param>
+ *          <param-name>maxAgeSeconds</param-name>
+ *          <param-value>31536000</param-value>
+ *       </init-param>
+ *       <init-param>
+ *          <param-name>includeSubDomains</param-name>
+ *          <param-value>true</param-value>
+ *       </init-param>
+ *    </filter>
+ *    ...
+ *    <filter-mapping>
+ *       <filter-name>HstsFilter</filter-name>
+ *       <url-pattern>/*</url-pattern>
+ *    </filter-mapping>
+ *    ...
+ * </web-app>
+ * 
+ * @author Jens Borgland + * @see RFC 6797 + */ +public class HstsFilter extends FilterBase { + + private static final String HEADER_NAME = "Strict-Transport-Security"; + private static final String MAX_AGE_DIRECTIVE = "max-age=%s"; + private static final String INCLUDE_SUB_DOMAINS_DIRECTIVE = "includeSubDomains"; + + private static final Log log = LogFactory.getLog(HstsFilter.class); + + // The default is "0" like recommended in section 11.2 of RFC 6797 + private int maxAgeSeconds = 0; + private boolean includeSubDomains = false; + + private String directives; + + public void setMaxAgeSeconds(int maxAgeSeconds) { + this.maxAgeSeconds = maxAgeSeconds; + } + + public void setIncludeSubDomains(boolean includeSubDomains) { + this.includeSubDomains = includeSubDomains; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + chain.doFilter(request, response); + + // Note that the HSTS header must not be included in HTTP responses + // conveyed over non-secure transport + if (request.isSecure() && response instanceof HttpServletResponse) { + HttpServletResponse res = (HttpServletResponse) response; + res.addHeader(HEADER_NAME, this.directives); + } + } + + @SuppressWarnings("boxing") + @Override + public void init(FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); + if (this.maxAgeSeconds < 0) { + throw new ServletException(sm.getString( + "hsts.invalidParameterValue", this.maxAgeSeconds, + "maxAgeSeconds")); + } + this.directives = String.format(MAX_AGE_DIRECTIVE, this.maxAgeSeconds); + if (this.includeSubDomains) { + this.directives += (" ; " + INCLUDE_SUB_DOMAINS_DIRECTIVE); + } + } + + @Override + protected Log getLogger() { + return log; + } + +} --- java/org/apache/catalina/filters/LocalStrings.properties (revision 1451166) +++ java/org/apache/catalina/filters/LocalStrings.properties (working copy) @@ -17,6 +17,7 @@ csrfPrevention.invalidRandomClass=Unable to create Random source using class [{0}] filterbase.noSuchProperty=The property "{0}" is not defined for filters of type "{1}" +hsts.invalidParameterValue="Invalid value "{0}" for parameter "{1}" http.403=Access to the specified resource ({0}) has been forbidden. expiresFilter.noExpirationConfigured=Request "{0}" with response status "{1}" content-type "{2}", no expiration configured --- test/org/apache/catalina/filters/TestHstsFilter.java (revision 0) +++ test/org/apache/catalina/filters/TestHstsFilter.java (working copy) @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.filters; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.deploy.FilterDef; +import org.apache.catalina.deploy.FilterMap; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestHstsFilter extends TomcatBaseTest { + + @Test + public void testIncludeDomainsTrue() throws Exception { + doTest("0", "true", true, true, "max-age=0 ; includeSubDomains"); + } + + @Test + public void testIncludeDomainsFalse() throws Exception { + doTest("0", "false", true, true, "max-age=0"); + } + + @Test + public void testNonZeroMaxAge() throws Exception { + doTest("31536000", "true", true, true, + "max-age=31536000 ; includeSubDomains"); + } + + @Test + public void testNoParameters() throws Exception { + doTest(null, null, true, true, "max-age=0"); + } + + @Test + public void testNoMaxAgeParameter() throws Exception { + doTest(null, "true", true, true, "max-age=0 ; includeSubDomains"); + } + + @Test + public void testNoIncludeSubDomainsParameter() throws Exception { + doTest("0", null, true, true, "max-age=0"); + } + + @Test + public void testNonSecure() throws Exception { + doTest("0", "false", false, false, null); + } + + public void doTest(String maxAge, String includeSubDomains, boolean secure, + boolean expectHeader, String expectedDirectives) throws Exception { + Tomcat tomcat = getTomcatInstance(); + tomcat.getConnector().setSecure(secure); + + Context ctx = tomcat.addContext("", + System.getProperty("java.io.tmpdir")); + + HttpServlet servlet = new HttpServlet() { + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest request, + HttpServletResponse response) throws ServletException, + IOException { + response.setContentType("text/plain"); + } + }; + Tomcat.addServlet(ctx, "servlet", servlet); + ctx.addServletMapping("/", "servlet"); + + FilterDef filterDef = new FilterDef(); + filterDef.setFilterClass(HstsFilter.class.getName()); + filterDef.setFilterName("filter"); + if (maxAge != null) { + filterDef.addInitParameter("maxAgeSeconds", maxAge); + } + if (includeSubDomains != null) { + filterDef.addInitParameter("includeSubDomains", includeSubDomains); + } + ctx.addFilterDef(filterDef); + FilterMap filterMap = new FilterMap(); + filterMap.setFilterName("filter"); + filterMap.addServletName("servlet"); + ctx.addFilterMap(filterMap); + + tomcat.start(); + + Map> headers = new HashMap<>(); + getUrl("http://localhost:" + getPort() + "/", new ByteChunk(), headers); + + List stsHeaders = headers.get("Strict-Transport-Security"); + if (expectHeader) { + Assert.assertNotNull("No Strict-Transport-Security header added", + stsHeaders); + Assert.assertEquals( + "Multiple Strict-Transport-Security headers added", 1, + stsHeaders.size()); + String directives = stsHeaders.get(0); + Assert.assertEquals( + "Incorrect Strict-Transport-Security directives", + expectedDirectives, directives); + } else { + Assert.assertNull( + "Strict-Transport-Security header added when it shouldn't have been", + stsHeaders); + } + } + +}