| /* |
| * 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.net.URI; |
| import java.net.URISyntaxException; |
| |
| 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; |
| |
| /** |
| * Provides a single configuration point for security measures that required the |
| * addition of one or more HTTP headers to the response. |
| */ |
| public class HttpHeaderSecurityFilter extends FilterBase { |
| |
| // Log must be non-static as loggers are created per class-loader and this |
| // Filter may be used in multiple class loaders |
| private final Log log = LogFactory.getLog(HttpHeaderSecurityFilter.class); // must not be static |
| |
| // HSTS |
| private static final String HSTS_HEADER_NAME = "Strict-Transport-Security"; |
| private boolean hstsEnabled = true; |
| private int hstsMaxAgeSeconds = 0; |
| private boolean hstsIncludeSubDomains = false; |
| private boolean hstsPreload = false; |
| private String hstsHeaderValue; |
| |
| // Click-jacking protection |
| private static final String ANTI_CLICK_JACKING_HEADER_NAME = "X-Frame-Options"; |
| private boolean antiClickJackingEnabled = true; |
| private XFrameOption antiClickJackingOption = XFrameOption.DENY; |
| private URI antiClickJackingUri; |
| private String antiClickJackingHeaderValue; |
| |
| // Block content sniffing |
| private static final String BLOCK_CONTENT_TYPE_SNIFFING_HEADER_NAME = "X-Content-Type-Options"; |
| private static final String BLOCK_CONTENT_TYPE_SNIFFING_HEADER_VALUE = "nosniff"; |
| private boolean blockContentTypeSniffingEnabled = true; |
| |
| // Cross-site scripting filter protection |
| private static final String XSS_PROTECTION_HEADER_NAME = "X-XSS-Protection"; |
| private static final String XSS_PROTECTION_HEADER_VALUE = "1; mode=block"; |
| private boolean xssProtectionEnabled = true; |
| |
| @Override |
| public void init(FilterConfig filterConfig) throws ServletException { |
| super.init(filterConfig); |
| |
| // Build HSTS header value |
| StringBuilder hstsValue = new StringBuilder("max-age="); |
| hstsValue.append(hstsMaxAgeSeconds); |
| if (hstsIncludeSubDomains) { |
| hstsValue.append(";includeSubDomains"); |
| } |
| if (hstsPreload) { |
| hstsValue.append(";preload"); |
| } |
| hstsHeaderValue = hstsValue.toString(); |
| |
| // Anti click-jacking |
| StringBuilder cjValue = new StringBuilder(antiClickJackingOption.headerValue); |
| if (antiClickJackingOption == XFrameOption.ALLOW_FROM) { |
| cjValue.append(' '); |
| cjValue.append(antiClickJackingUri); |
| } |
| antiClickJackingHeaderValue = cjValue.toString(); |
| } |
| |
| |
| @Override |
| public void doFilter(ServletRequest request, ServletResponse response, |
| FilterChain chain) throws IOException, ServletException { |
| |
| if (response instanceof HttpServletResponse) { |
| HttpServletResponse httpResponse = (HttpServletResponse) response; |
| |
| if (response.isCommitted()) { |
| throw new ServletException(sm.getString("httpHeaderSecurityFilter.committed")); |
| } |
| |
| // HSTS |
| if (hstsEnabled && request.isSecure()) { |
| httpResponse.setHeader(HSTS_HEADER_NAME, hstsHeaderValue); |
| } |
| |
| // anti click-jacking |
| if (antiClickJackingEnabled) { |
| httpResponse.setHeader(ANTI_CLICK_JACKING_HEADER_NAME, antiClickJackingHeaderValue); |
| } |
| |
| // Block content type sniffing |
| if (blockContentTypeSniffingEnabled) { |
| httpResponse.setHeader(BLOCK_CONTENT_TYPE_SNIFFING_HEADER_NAME, |
| BLOCK_CONTENT_TYPE_SNIFFING_HEADER_VALUE); |
| } |
| |
| // cross-site scripting filter protection |
| if (xssProtectionEnabled) { |
| httpResponse.setHeader(XSS_PROTECTION_HEADER_NAME, XSS_PROTECTION_HEADER_VALUE); |
| } |
| } |
| |
| chain.doFilter(request, response); |
| } |
| |
| |
| @Override |
| protected Log getLogger() { |
| return log; |
| } |
| |
| |
| @Override |
| protected boolean isConfigProblemFatal() { |
| // This filter is security related to configuration issues always |
| // trigger a failure. |
| return true; |
| } |
| |
| |
| public boolean isHstsEnabled() { |
| return hstsEnabled; |
| } |
| |
| |
| public void setHstsEnabled(boolean hstsEnabled) { |
| this.hstsEnabled = hstsEnabled; |
| } |
| |
| |
| public int getHstsMaxAgeSeconds() { |
| return hstsMaxAgeSeconds; |
| } |
| |
| |
| public void setHstsMaxAgeSeconds(int hstsMaxAgeSeconds) { |
| if (hstsMaxAgeSeconds < 0) { |
| this.hstsMaxAgeSeconds = 0; |
| } else { |
| this.hstsMaxAgeSeconds = hstsMaxAgeSeconds; |
| } |
| } |
| |
| |
| public boolean isHstsIncludeSubDomains() { |
| return hstsIncludeSubDomains; |
| } |
| |
| |
| public void setHstsIncludeSubDomains(boolean hstsIncludeSubDomains) { |
| this.hstsIncludeSubDomains = hstsIncludeSubDomains; |
| } |
| |
| |
| public boolean isHstsPreload() { |
| return hstsPreload; |
| } |
| |
| |
| public void setHstsPreload(boolean hstsPreload) { |
| this.hstsPreload = hstsPreload; |
| } |
| |
| |
| public boolean isAntiClickJackingEnabled() { |
| return antiClickJackingEnabled; |
| } |
| |
| |
| public void setAntiClickJackingEnabled(boolean antiClickJackingEnabled) { |
| this.antiClickJackingEnabled = antiClickJackingEnabled; |
| } |
| |
| |
| public String getAntiClickJackingOption() { |
| return antiClickJackingOption.toString(); |
| } |
| |
| |
| public void setAntiClickJackingOption(String antiClickJackingOption) { |
| for (XFrameOption option : XFrameOption.values()) { |
| if (option.getHeaderValue().equalsIgnoreCase(antiClickJackingOption)) { |
| this.antiClickJackingOption = option; |
| return; |
| } |
| } |
| throw new IllegalArgumentException( |
| sm.getString("httpHeaderSecurityFilter.clickjack.invalid", antiClickJackingOption)); |
| } |
| |
| |
| public String getAntiClickJackingUri() { |
| return antiClickJackingUri.toString(); |
| } |
| |
| |
| public boolean isBlockContentTypeSniffingEnabled() { |
| return blockContentTypeSniffingEnabled; |
| } |
| |
| |
| public void setBlockContentTypeSniffingEnabled( |
| boolean blockContentTypeSniffingEnabled) { |
| this.blockContentTypeSniffingEnabled = blockContentTypeSniffingEnabled; |
| } |
| |
| |
| public void setAntiClickJackingUri(String antiClickJackingUri) { |
| URI uri; |
| try { |
| uri = new URI(antiClickJackingUri); |
| } catch (URISyntaxException e) { |
| throw new IllegalArgumentException(e); |
| } |
| this.antiClickJackingUri = uri; |
| } |
| |
| |
| public boolean isXssProtectionEnabled() { |
| return xssProtectionEnabled; |
| } |
| |
| |
| public void setXssProtectionEnabled(boolean xssProtectionEnabled) { |
| this.xssProtectionEnabled = xssProtectionEnabled; |
| } |
| |
| |
| private enum XFrameOption { |
| DENY("DENY"), |
| SAME_ORIGIN("SAMEORIGIN"), |
| ALLOW_FROM("ALLOW-FROM"); |
| |
| |
| private final String headerValue; |
| |
| private XFrameOption(String headerValue) { |
| this.headerValue = headerValue; |
| } |
| |
| public String getHeaderValue() { |
| return headerValue; |
| } |
| } |
| } |