diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java index 5261f76777..0862dacfd6 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java @@ -18,6 +18,34 @@ package org.apache.nifi.web.server; import com.google.common.base.Strings; import com.google.common.collect.Lists; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.ServletContext; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.NiFiServer; @@ -47,7 +75,10 @@ import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.web.ContentAccess; import org.apache.nifi.web.NiFiWebConfigurationContext; import org.apache.nifi.web.UiExtensionType; -import org.apache.nifi.web.security.ContentSecurityPolicyFilter; +import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter; +import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter; +import org.apache.nifi.web.security.headers.XFrameOptionsFilter; +import org.apache.nifi.web.security.headers.XSSProtectionFilter; import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.DeploymentManager; @@ -79,41 +110,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; -import javax.servlet.DispatcherType; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletResponse; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.stream.Collectors; - /** * Encapsulates the Jetty instance. */ @@ -583,13 +579,13 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader { // configure the max form size (3x the default) webappContext.setMaxFormContentSize(600000); - // add a filter to set the X-Frame-Options filter - webappContext.addFilter(new FilterHolder(FRAME_OPTIONS_FILTER), "/*", EnumSet.allOf(DispatcherType.class)); - - // add a filter to set the Content Security Policy frame-ancestors directive - FilterHolder cspFilter = new FilterHolder(new ContentSecurityPolicyFilter()); - cspFilter.setName(ContentSecurityPolicyFilter.class.getSimpleName()); - webappContext.addFilter(cspFilter, "/*", EnumSet.allOf(DispatcherType.class)); + // add HTTP security headers to all responses + final String ALL_PATHS = "/*"; + ArrayList> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class)); + if(props.isHTTPSConfigured()) { + filters.add(StrictTransportSecurityFilter.class); + } + filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext)); try { // configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ... @@ -602,6 +598,12 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader { return webappContext; } + private void addFilters(Class clazz, String path, WebAppContext webappContext) { + FilterHolder holder = new FilterHolder(clazz); + holder.setName(clazz.getSimpleName()); + webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class)); + } + private void addDocsServlets(WebAppContext docsContext) { try { // Load the nifi/docs directory @@ -1148,30 +1150,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader { } } - private static final Filter FRAME_OPTIONS_FILTER = new Filter() { - private static final String FRAME_OPTIONS = "X-Frame-Options"; - private static final String SAME_ORIGIN = "SAMEORIGIN"; - - @Override - public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain) - throws IOException, ServletException { - - // set frame options accordingly - final HttpServletResponse response = (HttpServletResponse) resp; - response.setHeader(FRAME_OPTIONS, SAME_ORIGIN); - - filterChain.doFilter(req, resp); - } - - @Override - public void init(final FilterConfig config) { - } - - @Override - public void destroy() { - } - }; - /** * Holds the result of loading WARs for custom UIs. */ diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/JettyServerTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/JettyServerTest.java index d0b839686e..c6d161c8f5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/JettyServerTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/JettyServerTest.java @@ -17,33 +17,20 @@ package org.apache.nifi.web.server; +import static org.apache.nifi.security.util.KeyStoreUtils.SUN_PROVIDER_NAME; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; import org.apache.nifi.security.util.KeystoreType; import org.apache.nifi.util.NiFiProperties; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.Test; -import org.mockito.Mockito; -import org.springframework.mock.web.MockHttpServletResponse; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.util.HashMap; -import java.util.Map; - -import static org.apache.nifi.security.util.KeyStoreUtils.SUN_PROVIDER_NAME; -import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class JettyServerTest { @Test @@ -155,29 +142,4 @@ public class JettyServerTest { verify(contextFactory).setTrustStoreType(trustStoreType); verify(contextFactory).setTrustStoreProvider(BouncyCastleProvider.PROVIDER_NAME); } - - @Test - public void testNoDuplicateXFrameOptions() throws NoSuchFieldException, IllegalAccessException, ServletException, IOException { - Field xOptionsFilter = JettyServer.class.getDeclaredField("FRAME_OPTIONS_FILTER"); - xOptionsFilter.setAccessible(true); - Filter filter = (Filter) xOptionsFilter.get(xOptionsFilter); - - HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); - Mockito.when(mockRequest.getRequestURI()).thenReturn("/"); - - MockHttpServletResponse mockResponse = new MockHttpServletResponse(); - FilterChain mockFilterChain = Mockito.mock(FilterChain.class); - ServletContext mockContext = Mockito.mock(ServletContext.class); - FilterConfig mockFilterConfig = Mockito.mock(FilterConfig.class); - - when(mockFilterConfig.getServletContext()).thenReturn(mockContext); - - filter.init(mockFilterConfig); - - // Call doFilter twice, then check the header only appears once. - filter.doFilter(mockRequest, mockResponse, mockFilterChain); - filter.doFilter(mockRequest, mockResponse, mockFilterChain); - - assertEquals(1, mockResponse.getHeaders("X-Frame-Options").size()); - } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ContentSecurityPolicyFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/ContentSecurityPolicyFilter.java similarity index 93% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ContentSecurityPolicyFilter.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/ContentSecurityPolicyFilter.java index 0f8a97742e..ca4cd0f74f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ContentSecurityPolicyFilter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/ContentSecurityPolicyFilter.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.web.security; +package org.apache.nifi.web.security.headers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,7 +28,7 @@ import javax.servlet.Filter; import javax.servlet.FilterConfig; /** - * A filter to apply the Content Security Policy (which supersedes the X-Frame-Options header). + * A filter to apply the Content Security Policy header. * */ public class ContentSecurityPolicyFilter implements Filter { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/StrictTransportSecurityFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/StrictTransportSecurityFilter.java new file mode 100644 index 0000000000..62468d7633 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/StrictTransportSecurityFilter.java @@ -0,0 +1,58 @@ +/* + * 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.nifi.web.security.headers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +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 java.io.IOException; + +/** + * A filter to apply the HTTP Strict Transport Security (HSTS) HTTP header. This forces the browser to use HTTPS for + * all + */ +public class StrictTransportSecurityFilter implements Filter { + private static final String HEADER = "Strict-Transport-Security"; + private static final String POLICY = "max-age=31540000"; + + private static final Logger logger = LoggerFactory.getLogger(StrictTransportSecurityFilter.class); + + @Override + public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain) + throws IOException, ServletException { + + final HttpServletResponse response = (HttpServletResponse) resp; + response.setHeader(HEADER, POLICY); + + filterChain.doFilter(req, resp); + } + + @Override + public void init(final FilterConfig config) { + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/XFrameOptionsFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/XFrameOptionsFilter.java new file mode 100644 index 0000000000..44a1ac21f7 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/XFrameOptionsFilter.java @@ -0,0 +1,58 @@ +/* + * 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.nifi.web.security.headers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +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 java.io.IOException; + +/** + * A filter to apply the X-Frame-Options header. + * + */ +public class XFrameOptionsFilter implements Filter { + private static final String HEADER = "X-Frame-Options"; + private static final String POLICY = "SAMEORIGIN"; + + private static final Logger logger = LoggerFactory.getLogger(XFrameOptionsFilter.class); + + @Override + public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain) + throws IOException, ServletException { + + final HttpServletResponse response = (HttpServletResponse) resp; + response.setHeader(HEADER, POLICY); + + filterChain.doFilter(req, resp); + } + + @Override + public void init(final FilterConfig config) { + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/XSSProtectionFilter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/XSSProtectionFilter.java new file mode 100644 index 0000000000..4fdf4ac8d9 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/headers/XSSProtectionFilter.java @@ -0,0 +1,58 @@ +/* + * 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.nifi.web.security.headers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +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 java.io.IOException; + +/** + * A filter to apply the Cross Site Scripting (XSS) HTTP header. Protects against reflected cross-site scripting attacks. + * The browser will prevent rendering of the page if an attack is detected. + */ +public class XSSProtectionFilter implements Filter { + private static final String HEADER = "X-XSS-Protection"; + private static final String POLICY = "1; mode=block"; + + private static final Logger logger = LoggerFactory.getLogger(XSSProtectionFilter.class); + + @Override + public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain filterChain) + throws IOException, ServletException { + + final HttpServletResponse response = (HttpServletResponse) resp; + response.setHeader(HEADER, POLICY); + + filterChain.doFilter(req, resp); + } + + @Override + public void init(final FilterConfig config) { + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/ContentSecurityPolicyFilterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/headers/HTTPHeaderFiltersTest.java similarity index 54% rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/ContentSecurityPolicyFilterTest.java rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/headers/HTTPHeaderFiltersTest.java index 7bfb9a4d22..5744b52898 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/ContentSecurityPolicyFilterTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/headers/HTTPHeaderFiltersTest.java @@ -14,8 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.nifi.web.security; +package org.apache.nifi.web.security.headers; +import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter; import org.eclipse.jetty.servlet.FilterHolder; import org.junit.Test; import org.mockito.Mockito; @@ -28,7 +29,7 @@ import java.io.IOException; import static org.junit.Assert.assertEquals; -public class ContentSecurityPolicyFilterTest { +public class HTTPHeaderFiltersTest { @Test public void testCSPHeaderApplied() throws ServletException, IOException { @@ -67,4 +68,60 @@ public class ContentSecurityPolicyFilterTest { assertEquals("frame-ancestors 'self'", mockResponse.getHeader("Content-Security-Policy")); } + + @Test + public void testXFrameOptionsHeaderApplied() throws ServletException, IOException { + // Arrange + + FilterHolder xfoFilter = new FilterHolder(new XFrameOptionsFilter()); + + // Set up request + HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = Mockito.mock(FilterChain.class); + + // Action + xfoFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain); + + // Verify + assertEquals("SAMEORIGIN", mockResponse.getHeader("X-Frame-Options")); + } + + @Test + public void testHSTSHeaderApplied() throws ServletException, IOException { + // Arrange + + FilterHolder hstsFilter = new FilterHolder(new StrictTransportSecurityFilter()); + + // Set up request + HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = Mockito.mock(FilterChain.class); + + // Action + hstsFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain); + + // Verify + assertEquals("max-age=31540000", mockResponse.getHeader("Strict-Transport-Security")); + } + + @Test + public void testXSSProtectionHeaderApplied() throws ServletException, IOException { + // Arrange + + FilterHolder xssFilter = new FilterHolder(new XSSProtectionFilter()); + + // Set up request + HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = Mockito.mock(FilterChain.class); + + // Action + xssFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain); + + // Verify + assertEquals("1; mode=block", mockResponse.getHeader("X-XSS-Protection")); + } + + } \ No newline at end of file