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 66ecd451de95..3bf98ca620dc 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 @@ -74,6 +74,7 @@ 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.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; @@ -502,6 +503,11 @@ private WebAppContext loadWar(final File warFile, final String contextPath, fina // 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)); + try { // configure the class loader - webappClassLoader -> jetty nar -> web app's nar -> ... webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext)); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml index 75a19a3e3a53..646b7e85f716 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/pom.xml @@ -154,5 +154,16 @@ jettison test + + org.springframework + spring-test + 5.0.6.RELEASE + test + + + org.eclipse.jetty + jetty-servlet + test + 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/ContentSecurityPolicyFilter.java new file mode 100644 index 000000000000..0f8a97742e8b --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/ContentSecurityPolicyFilter.java @@ -0,0 +1,57 @@ +/* + * 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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterConfig; + +/** + * A filter to apply the Content Security Policy (which supersedes the X-Frame-Options header). + * + */ +public class ContentSecurityPolicyFilter implements Filter { + private static final String HEADER = "Content-Security-Policy"; + private static final String POLICY = "frame-ancestors 'self'"; + + private static final Logger logger = LoggerFactory.getLogger(ContentSecurityPolicyFilter.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/ContentSecurityPolicyFilterTest.java new file mode 100644 index 000000000000..7bfb9a4d22d0 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/ContentSecurityPolicyFilterTest.java @@ -0,0 +1,70 @@ +/* + * 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; + +import org.eclipse.jetty.servlet.FilterHolder; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletResponse; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class ContentSecurityPolicyFilterTest { + + @Test + public void testCSPHeaderApplied() throws ServletException, IOException { + // Arrange + + FilterHolder originFilter = new FilterHolder(new ContentSecurityPolicyFilter()); + + // Set up request + HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = Mockito.mock(FilterChain.class); + + // Action + originFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain); + + // Verify + assertEquals("frame-ancestors 'self'", mockResponse.getHeader("Content-Security-Policy")); + } + + @Test + public void testCSPHeaderAppliedOnlyOnce() throws ServletException, IOException { + // Arrange + + FilterHolder originFilter = new FilterHolder(new ContentSecurityPolicyFilter()); + + // Set up request + HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse mockResponse = new MockHttpServletResponse(); + FilterChain mockFilterChain = Mockito.mock(FilterChain.class); + + // Action + originFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain); + originFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain); + + // Verify + assertEquals("frame-ancestors 'self'", mockResponse.getHeader("Content-Security-Policy")); + } + +} \ No newline at end of file