View | Details | Raw Unified | Return to bug 54618
Collapse All | Expand All

(-)java/org/apache/catalina/filters/HstsFilter.java (+123 lines)
Line 0 Link Here
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one or more
3
 * contributor license agreements.  See the NOTICE file distributed with
4
 * this work for additional information regarding copyright ownership.
5
 * The ASF licenses this file to You under the Apache License, Version 2.0
6
 * (the "License"); you may not use this file except in compliance with
7
 * the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
package org.apache.catalina.filters;
18
19
import java.io.IOException;
20
21
import javax.servlet.FilterChain;
22
import javax.servlet.FilterConfig;
23
import javax.servlet.ServletException;
24
import javax.servlet.ServletRequest;
25
import javax.servlet.ServletResponse;
26
import javax.servlet.http.HttpServletResponse;
27
28
import org.apache.juli.logging.Log;
29
import org.apache.juli.logging.LogFactory;
30
31
/**
32
 * Implements HTTP Strict Transport Security (HSTS) according to RFC 6797.
33
 * <p>
34
 * The filter assumes that:
35
 * <ul>
36
 * <li>The filter is mapped to /*</li>
37
 * <li>The connector is configured to use only SSL/TLS</li>
38
 * </ul>
39
 * <h1>Filter Configuration</h1>
40
 * <h2>Basic configuration to add &#x27;
41
 * <tt>Strict-Transport-Security: max-age=31536000 ; includeSubDomains</tt>
42
 * &#x27; to all responses sent over SSL/TLS</h2> <code><pre>
43
 * &lt;web-app ...&gt;
44
 *    ...
45
 *    &lt;filter&gt;
46
 *       &lt;filter-name&gt;HstsFilter&lt;/filter-name&gt;
47
 *       &lt;filter-class&gt;org.apache.catalina.filters.HstsFilter&lt;/filter-class&gt;
48
 *       &lt;init-param&gt;
49
 *          &lt;param-name&gt;maxAgeSeconds&lt;/param-name&gt;
50
 *          &lt;param-value&gt;31536000&lt;/param-value&gt;
51
 *       &lt;/init-param&gt;
52
 *       &lt;init-param&gt;
53
 *          &lt;param-name&gt;includeSubDomains&lt;/param-name&gt;
54
 *          &lt;param-value&gt;true&lt;/param-value&gt;
55
 *       &lt;/init-param&gt;
56
 *    &lt;/filter&gt;
57
 *    ...
58
 *    &lt;filter-mapping&gt;
59
 *       &lt;filter-name&gt;HstsFilter&lt;/filter-name&gt;
60
 *       &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
61
 *    &lt;/filter-mapping&gt;
62
 *    ...
63
 * &lt;/web-app&gt;
64
 * </pre></code>
65
 * @author Jens Borgland
66
 * @see <a href="http://tools.ietf.org/rfc/rfc6797.txt">RFC 6797</a>
67
 */
68
public class HstsFilter extends FilterBase {
69
70
    private static final String HEADER_NAME = "Strict-Transport-Security";
71
    private static final String MAX_AGE_DIRECTIVE = "max-age=%s";
72
    private static final String INCLUDE_SUB_DOMAINS_DIRECTIVE = "includeSubDomains";
73
74
    private static final Log log = LogFactory.getLog(HstsFilter.class);
75
76
    // The default is "0" like recommended in section 11.2 of RFC 6797
77
    private int maxAgeSeconds = 0;
78
    private boolean includeSubDomains = false;
79
80
    private String directives;
81
82
    public void setMaxAgeSeconds(int maxAgeSeconds) {
83
        this.maxAgeSeconds = maxAgeSeconds;
84
    }
85
86
    public void setIncludeSubDomains(boolean includeSubDomains) {
87
        this.includeSubDomains = includeSubDomains;
88
    }
89
90
    @Override
91
    public void doFilter(ServletRequest request, ServletResponse response,
92
            FilterChain chain) throws IOException, ServletException {
93
        chain.doFilter(request, response);
94
95
        // Note that the HSTS header must not be included in HTTP responses
96
        // conveyed over non-secure transport
97
        if (request.isSecure() && response instanceof HttpServletResponse) {
98
            HttpServletResponse res = (HttpServletResponse) response;
99
            res.addHeader(HEADER_NAME, this.directives);
100
        }
101
    }
102
103
    @SuppressWarnings("boxing")
104
    @Override
105
    public void init(FilterConfig filterConfig) throws ServletException {
106
        super.init(filterConfig);
107
        if (this.maxAgeSeconds < 0) {
108
            throw new ServletException(sm.getString(
109
                    "hsts.invalidParameterValue", this.maxAgeSeconds,
110
                    "maxAgeSeconds"));
111
        }
112
        this.directives = String.format(MAX_AGE_DIRECTIVE, this.maxAgeSeconds);
113
        if (this.includeSubDomains) {
114
            this.directives += (" ; " + INCLUDE_SUB_DOMAINS_DIRECTIVE);
115
        }
116
    }
117
118
    @Override
119
    protected Log getLogger() {
120
        return log;
121
    }
122
123
}
(-)java/org/apache/catalina/filters/LocalStrings.properties (+1 lines)
Lines 17-22 Link Here
17
csrfPrevention.invalidRandomClass=Unable to create Random source using class [{0}]
17
csrfPrevention.invalidRandomClass=Unable to create Random source using class [{0}]
18
filterbase.noSuchProperty=The property "{0}" is not defined for filters of type "{1}"
18
filterbase.noSuchProperty=The property "{0}" is not defined for filters of type "{1}"
19
19
20
hsts.invalidParameterValue="Invalid value "{0}" for parameter "{1}"
20
http.403=Access to the specified resource ({0}) has been forbidden.
21
http.403=Access to the specified resource ({0}) has been forbidden.
21
22
22
expiresFilter.noExpirationConfigured=Request "{0}" with response status "{1}" content-type "{2}", no expiration configured
23
expiresFilter.noExpirationConfigured=Request "{0}" with response status "{1}" content-type "{2}", no expiration configured
(-)test/org/apache/catalina/filters/TestHstsFilter.java (+136 lines)
Line 0 Link Here
1
/*
2
 *  Licensed to the Apache Software Foundation (ASF) under one or more
3
 *  contributor license agreements.  See the NOTICE file distributed with
4
 *  this work for additional information regarding copyright ownership.
5
 *  The ASF licenses this file to You under the Apache License, Version 2.0
6
 *  (the "License"); you may not use this file except in compliance with
7
 *  the License.  You may obtain a copy of the License at
8
 *
9
 *      http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 *  Unless required by applicable law or agreed to in writing, software
12
 *  distributed under the License is distributed on an "AS IS" BASIS,
13
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 *  See the License for the specific language governing permissions and
15
 *  limitations under the License.
16
 */
17
package org.apache.catalina.filters;
18
19
import java.io.IOException;
20
import java.util.HashMap;
21
import java.util.List;
22
import java.util.Map;
23
24
import javax.servlet.ServletException;
25
import javax.servlet.http.HttpServlet;
26
import javax.servlet.http.HttpServletRequest;
27
import javax.servlet.http.HttpServletResponse;
28
29
import org.junit.Assert;
30
import org.junit.Test;
31
32
import org.apache.catalina.Context;
33
import org.apache.catalina.deploy.FilterDef;
34
import org.apache.catalina.deploy.FilterMap;
35
import org.apache.catalina.startup.Tomcat;
36
import org.apache.catalina.startup.TomcatBaseTest;
37
import org.apache.tomcat.util.buf.ByteChunk;
38
39
public class TestHstsFilter extends TomcatBaseTest {
40
41
    @Test
42
    public void testIncludeDomainsTrue() throws Exception {
43
        doTest("0", "true", true, true, "max-age=0 ; includeSubDomains");
44
    }
45
46
    @Test
47
    public void testIncludeDomainsFalse() throws Exception {
48
        doTest("0", "false", true, true, "max-age=0");
49
    }
50
51
    @Test
52
    public void testNonZeroMaxAge() throws Exception {
53
        doTest("31536000", "true", true, true,
54
                "max-age=31536000 ; includeSubDomains");
55
    }
56
57
    @Test
58
    public void testNoParameters() throws Exception {
59
        doTest(null, null, true, true, "max-age=0");
60
    }
61
62
    @Test
63
    public void testNoMaxAgeParameter() throws Exception {
64
        doTest(null, "true", true, true, "max-age=0 ; includeSubDomains");
65
    }
66
67
    @Test
68
    public void testNoIncludeSubDomainsParameter() throws Exception {
69
        doTest("0", null, true, true, "max-age=0");
70
    }
71
72
    @Test
73
    public void testNonSecure() throws Exception {
74
        doTest("0", "false", false, false, null);
75
    }
76
77
    public void doTest(String maxAge, String includeSubDomains, boolean secure,
78
            boolean expectHeader, String expectedDirectives) throws Exception {
79
        Tomcat tomcat = getTomcatInstance();
80
        tomcat.getConnector().setSecure(secure);
81
82
        Context ctx = tomcat.addContext("",
83
                System.getProperty("java.io.tmpdir"));
84
85
        HttpServlet servlet = new HttpServlet() {
86
            private static final long serialVersionUID = 1L;
87
88
            @Override
89
            protected void service(HttpServletRequest request,
90
                    HttpServletResponse response) throws ServletException,
91
                    IOException {
92
                response.setContentType("text/plain");
93
            }
94
        };
95
        Tomcat.addServlet(ctx, "servlet", servlet);
96
        ctx.addServletMapping("/", "servlet");
97
98
        FilterDef filterDef = new FilterDef();
99
        filterDef.setFilterClass(HstsFilter.class.getName());
100
        filterDef.setFilterName("filter");
101
        if (maxAge != null) {
102
            filterDef.addInitParameter("maxAgeSeconds", maxAge);
103
        }
104
        if (includeSubDomains != null) {
105
            filterDef.addInitParameter("includeSubDomains", includeSubDomains);
106
        }
107
        ctx.addFilterDef(filterDef);
108
        FilterMap filterMap = new FilterMap();
109
        filterMap.setFilterName("filter");
110
        filterMap.addServletName("servlet");
111
        ctx.addFilterMap(filterMap);
112
113
        tomcat.start();
114
115
        Map<String, List<String>> headers = new HashMap<>();
116
        getUrl("http://localhost:" + getPort() + "/", new ByteChunk(), headers);
117
118
        List<String> stsHeaders = headers.get("Strict-Transport-Security");
119
        if (expectHeader) {
120
            Assert.assertNotNull("No Strict-Transport-Security header added",
121
                    stsHeaders);
122
            Assert.assertEquals(
123
                    "Multiple Strict-Transport-Security headers added", 1,
124
                    stsHeaders.size());
125
            String directives = stsHeaders.get(0);
126
            Assert.assertEquals(
127
                    "Incorrect Strict-Transport-Security directives",
128
                    expectedDirectives, directives);
129
        } else {
130
            Assert.assertNull(
131
                    "Strict-Transport-Security header added when it shouldn't have been",
132
                    stsHeaders);
133
        }
134
    }
135
136
}

Return to bug 54618