1
0
mirror of synced 2026-08-04 17:27:13 +00:00

SEC-2230: HTTP Strict Transport Security (HSTS)Add support for Strict

This is a distinct filter as apposed to reusing StaticHeaderWriter
since the specification specifies that the "Strict-Transport-Security"
header should only be set on secure requests. It would not make sense to
require DelegatingRequestMatcherHeaderWriter since this requirement is
in the specification.
This commit is contained in:
Rob Winch
2013-07-29 18:22:28 -05:00
parent 8013cd54d6
commit c85328c5d1
7 changed files with 416 additions and 1 deletions
@@ -0,0 +1,165 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed 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.springframework.security.web.headers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.util.RequestMatcher;
import org.springframework.util.Assert;
/**
* Provides support for <a href="http://tools.ietf.org/html/rfc6797">HTTP Strict
* Transport Security (HSTS)</a>.
*
* <p>
* By default the expiration is one year and subdomains will be included. This
* can be customized using {@link #setMaxAgeInSeconds(long)} and
* {@link #setIncludeSubDomains(boolean)} respectively.
* </p>
*
* <p>
* Since <a href="http://tools.ietf.org/html/rfc6797#section-7.2">section
* 7.2</a> states that HSTS Host MUST NOT include the STS header in HTTP
* responses, the default behavior is that the "Strict-Transport-Security" will
* only be added when {@link HttpServletRequest#isSecure()} returns {@code true}
* . At times this may need to be customized. For example, in some situations
* where SSL termination is used, something else may be used to determine if SSL
* was used. For these circumstances, {@link #setRequestMatcher(RequestMatcher)}
* can be invoked with a custom {@link RequestMatcher}.
* </p>
*
* @author Rob Winch
* @since 3.2
*/
public final class HstsHeaderWriter implements HeaderWriter {
private static final String HSTS_HEADER_NAME = "Strict-Transport-Security";
private final Log logger = LogFactory.getLog(getClass());
private RequestMatcher requestMatcher = new SecureRequestMatcher();
private long maxAgeInSeconds;
private boolean includeSubDomains;
private String hstsHeaderValue;
public HstsHeaderWriter() {
this.maxAgeInSeconds = 31536000;
this.includeSubDomains = true;
updateHstsHeaderValue();
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.security.web.headers.HeaderWriter#writeHeaders(javax
* .servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
public void writeHeaders(HttpServletRequest request,
HttpServletResponse response) {
if (requestMatcher.matches(request)) {
response.setHeader(HSTS_HEADER_NAME, hstsHeaderValue);
} else if (logger.isDebugEnabled()) {
logger.debug("Not injecting HSTS header since it did not match the requestMatcher "
+ requestMatcher);
}
}
/**
* Sets the {@link RequestMatcher} used to determine if the
* "Strict-Transport-Security" should be added. If true the header is added,
* else the header is not added. By default the header is added when
* {@link HttpServletRequest#isSecure()} returns true.
*
* @param requestMatcher
* the {@link RequestMatcher} to use.
* @throws IllegalArgumentException
* if {@link RequestMatcher} is null
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requestMatcher = requestMatcher;
}
/**
* <p>
* Sets the value (in seconds) for the max-age directive of the
* Strict-Transport-Security header. The default is one year.
* </p>
*
* <p>
* This instructs browsers how long to remember to keep this domain as a
* known HSTS Host. See <a
* href="http://tools.ietf.org/html/rfc6797#section-6.1.1">Section 6.1.1</a>
* for additional details.
* </p>
*
* @param maxAgeInSeconds
* the maximum amount of time (in seconds) to consider this
* domain as a known HSTS Host.
* @throws IllegalArgumentException
* if maxAgeInSeconds is negative
*/
public void setMaxAgeInSeconds(long maxAgeInSeconds) {
if (maxAgeInSeconds < 0) {
throw new IllegalArgumentException(
"maxAgeInSeconds must be non-negative. Got "
+ maxAgeInSeconds);
}
this.maxAgeInSeconds = maxAgeInSeconds;
updateHstsHeaderValue();
}
/**
* <p>
* If true, subdomains should be considered HSTS Hosts too. The default is
* true.
* </p>
*
* <p>
* See <a href="http://tools.ietf.org/html/rfc6797#section-6.1.2">Section
* 6.1.2</a> for additional details.
* </p>
*
* @param includeSubDomains
* true to include subdomains, else false
*/
public void setIncludeSubDomains(boolean includeSubDomains) {
this.includeSubDomains = includeSubDomains;
updateHstsHeaderValue();
}
private void updateHstsHeaderValue() {
String headerValue = "max-age=" + maxAgeInSeconds;
if (includeSubDomains) {
headerValue += " ; includeSubDomains";
}
this.hstsHeaderValue = headerValue;
}
private static final class SecureRequestMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
return request.isSecure();
}
}
}
@@ -0,0 +1,90 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed 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.springframework.security.web.headers;
import static org.fest.assertions.Assertions.assertThat;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
/**
* @author Rob Winch
*
*/
public class HstsHeaderWriterTests {
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private HstsHeaderWriter writer;
@Before
public void setup() {
request = new MockHttpServletRequest();
request.setSecure(true);
response = new MockHttpServletResponse();
writer = new HstsHeaderWriter();
}
@Test
public void writeHeadersDefaultValues() {
writer.writeHeaders(request, response);
assertThat(response.getHeaderNames().size()).isEqualTo(1);
assertThat(response.getHeader("Strict-Transport-Security")).isEqualTo("max-age=31536000 ; includeSubDomains");
}
@Test
public void writeHeadersIncludeSubDomainsFalse() {
writer.setIncludeSubDomains(false);
writer.writeHeaders(request, response);
assertThat(response.getHeaderNames().size()).isEqualTo(1);
assertThat(response.getHeader("Strict-Transport-Security")).isEqualTo("max-age=31536000");
}
@Test
public void writeHeadersCustomMaxAgeInSeconds() {
writer.setMaxAgeInSeconds(1);
writer.writeHeaders(request, response);
assertThat(response.getHeaderNames().size()).isEqualTo(1);
assertThat(response.getHeader("Strict-Transport-Security")).isEqualTo("max-age=1 ; includeSubDomains");
}
@Test
public void writeHeadersInsecureRequestDoesNotWriteHeader() {
request.setSecure(false);
writer.writeHeaders(request, response);
assertThat(response.getHeaderNames().isEmpty()).isTrue();
}
@Test(expected = IllegalArgumentException.class)
public void setMaxAgeInSecondsToNegative() {
writer.setMaxAgeInSeconds(-1);
}
@Test(expected = IllegalArgumentException.class)
public void setRequestMatcherToNull() {
writer.setRequestMatcher(null);
}
}