This is an automated email from the ASF dual-hosted git repository. thenatog pushed a commit to branch NIFIREG-320 in repository https://gitbox.apache.org/repos/asf/nifi-registry.git
commit 5cf41c8d9a41981af2491501b8bcb7d72bf61ef8 Author: thenatog <[email protected]> AuthorDate: Mon Sep 23 13:16:57 2019 -0400 Added jetty header filters to set security headers. Setting security headers for the registry-api using spring security configuration. --- .../apache/nifi/registry/jetty/JettyServer.java | 117 +++++++++++++-------- .../jetty/headers/ContentSecurityPolicyFilter.java | 58 ++++++++++ .../headers/StrictTransportSecurityFilter.java | 58 ++++++++++ .../jetty/headers/XFrameOptionsFilter.java | 58 ++++++++++ .../jetty/headers/XSSProtectionFilter.java | 59 +++++++++++ .../properties/NiFiRegistryProperties.java | 4 + nifi-registry-core/nifi-registry-web-api/pom.xml | 6 ++ .../web/security/NiFiRegistrySecurityConfig.java | 6 ++ 8 files changed, 322 insertions(+), 44 deletions(-) diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java index 45619f7..58b6249 100644 --- a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java +++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java @@ -17,6 +17,10 @@ package org.apache.nifi.registry.jetty; import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.jetty.headers.ContentSecurityPolicyFilter; +import org.apache.nifi.registry.jetty.headers.StrictTransportSecurityFilter; +import org.apache.nifi.registry.jetty.headers.XFrameOptionsFilter; +import org.apache.nifi.registry.jetty.headers.XSSProtectionFilter; import org.apache.nifi.registry.properties.NiFiRegistryProperties; import org.apache.nifi.registry.security.crypto.CryptoKeyProvider; import org.eclipse.jetty.annotations.AnnotationConfiguration; @@ -28,11 +32,10 @@ import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.ResourceHandler; -import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.webapp.Configuration; @@ -42,6 +45,8 @@ import org.eclipse.jetty.webapp.WebAppContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; import java.io.File; import java.io.FileFilter; import java.io.IOException; @@ -55,6 +60,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; import java.util.Enumeration; import java.util.HashSet; import java.util.LinkedList; @@ -106,6 +112,37 @@ public class JettyServer { } } + /** + * Returns a File object for the directory containing NIFI documentation. + * <p> + * Formerly, if the docsDirectory did not exist NIFI would fail to start + * with an IllegalStateException and a rather unhelpful log message. + * NIFI-2184 updates the process such that if the docsDirectory does not + * exist an attempt will be made to create the directory. If that is + * successful NIFI will no longer fail and will start successfully barring + * any other errors. The side effect of the docsDirectory not being present + * is that the documentation links under the 'General' portion of the help + * page will not be accessible, but at least the process will be running. + * + * @param docsDirectory Name of documentation directory in installation directory. + * @return A File object to the documentation directory; else startUpFailure called. + */ + private File getDocsDir(final String docsDirectory) { + File docsDir; + try { + docsDir = Paths.get(docsDirectory).toRealPath().toFile(); + } catch (IOException ex) { + logger.info("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable."); + docsDir = new File(docsDirectory).getAbsoluteFile(); + final boolean made = docsDir.mkdirs(); + if (!made) { + logger.error("Failed to create 'docs' directory!"); + startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created")); + } + } + return docsDir; + } + private void configureConnectors() { // create the http configuration final HttpConfiguration httpConfiguration = new HttpConfiguration(); @@ -254,11 +291,11 @@ public class JettyServer { final String docsContextPath = "/nifi-registry-docs"; webDocsContext = loadWar(webDocsWar, docsContextPath); + addDocsServlets(webDocsContext); final HandlerCollection handlers = new HandlerCollection(); handlers.addHandler(webUiContext); handlers.addHandler(webApiContext); - handlers.addHandler(createDocsWebApp(docsContextPath)); handlers.addHandler(webDocsContext); server.setHandler(handlers); } @@ -301,6 +338,15 @@ public class JettyServer { // configure the max form size (3x the default) webappContext.setMaxFormContentSize(600000); + // add HTTP security headers to all responses + final String ALL_PATHS = "/*"; + ArrayList<Class<? extends Filter>> filters = new ArrayList<>(Arrays.asList(XFrameOptionsFilter.class, ContentSecurityPolicyFilter.class, XSSProtectionFilter.class)); + if(properties.isHTTPSConfigured()) { + filters.add(StrictTransportSecurityFilter.class); + } + + filters.forEach( (filter) -> addFilters(filter, ALL_PATHS, webappContext)); + // start out assuming the system ClassLoader will be the parent, but if additional resources were specified then // inject a new ClassLoader in between the system and webapp ClassLoaders that contains the additional resources ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader(); @@ -315,6 +361,12 @@ public class JettyServer { return webappContext; } + private void addFilters(Class<? extends Filter> clazz, String path, WebAppContext webappContext) { + FilterHolder holder = new FilterHolder(clazz); + holder.setName(clazz.getSimpleName()); + webappContext.addFilter(holder, path, EnumSet.allOf(DispatcherType.class)); + } + private URL[] getWebApiAdditionalClasspath() { final String dbDriverDir = properties.getDatabaseDriverDirectory(); @@ -370,51 +422,28 @@ public class JettyServer { return resources.toArray(new URL[resources.size()]); } - private ContextHandler createDocsWebApp(final String contextPath) throws IOException { - final ResourceHandler resourceHandler = new ResourceHandler(); - resourceHandler.setDirectoriesListed(false); - - // load the docs directory - String docsDirectory = docsLocation; - if (StringUtils.isBlank(docsDirectory)) { - docsDirectory = "docs"; - } - - File docsDir; + private void addDocsServlets(WebAppContext docsContext) { try { - docsDir = Paths.get(docsDirectory).toRealPath().toFile(); - } catch (IOException ex) { - logger.warn("Directory '" + docsDirectory + "' is missing. Some documentation will be unavailable."); - docsDir = new File(docsDirectory).getAbsoluteFile(); - final boolean made = docsDir.mkdirs(); - if (!made) { - logger.error("Failed to create 'docs' directory!"); - startUpFailure(new IOException(docsDir.getAbsolutePath() + " could not be created")); - } - } + // Load the nifi-registry/docs directory + final File docsDir = getDocsDir(docsLocation); - final Resource docsResource = Resource.newResource(docsDir); + // Create the servlet which will serve the static resources + ServletHolder defaultHolder = new ServletHolder("default", DefaultServlet.class); + defaultHolder.setInitParameter("dirAllowed", "false"); - // load the rest documentation - final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs"); - if (!webApiDocsDir.exists()) { - final boolean made = webApiDocsDir.mkdirs(); - if (!made) { - throw new RuntimeException(webApiDocsDir.getAbsolutePath() + " could not be created"); - } - } - final Resource webApiDocsResource = Resource.newResource(webApiDocsDir); + ServletHolder docs = new ServletHolder("docs", DefaultServlet.class); + docs.setInitParameter("resourceBase", docsDir.getPath()); + docs.setInitParameter("dirAllowed", "false"); - // create resources for both docs locations - final ResourceCollection resources = new ResourceCollection(docsResource, webApiDocsResource); - resourceHandler.setBaseResource(resources); + docsContext.addServlet(docs, "/html/*"); + docsContext.addServlet(defaultHolder, "/"); - // create the context handler - final ContextHandler handler = new ContextHandler(contextPath); - handler.setHandler(resourceHandler); + logger.info("Loading documents web app with context path set to " + docsContext.getContextPath()); - logger.info("Loading documents web app with context path set to " + contextPath); - return handler; + } catch (Exception ex) { + logger.error("Unhandled Exception in createDocsWebApp: " + ex.getMessage()); + startUpFailure(ex); + } } public void start() { diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.java new file mode 100644 index 0000000..758e939 --- /dev/null +++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/ContentSecurityPolicyFilter.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.registry.jetty.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 Content Security Policy 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-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/StrictTransportSecurityFilter.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/StrictTransportSecurityFilter.java new file mode 100644 index 0000000..7f0f913 --- /dev/null +++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/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.registry.jetty.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-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XFrameOptionsFilter.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XFrameOptionsFilter.java new file mode 100644 index 0000000..fad5bbc --- /dev/null +++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/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.registry.jetty.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-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java new file mode 100644 index 0000000..62792f1 --- /dev/null +++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/headers/XSSProtectionFilter.java @@ -0,0 +1,59 @@ +/* + * 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.registry.jetty.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-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java index 31133eb..e1e9a39 100644 --- a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java +++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java @@ -118,6 +118,10 @@ public class NiFiRegistryProperties extends Properties { return getPropertyAsInteger(WEB_HTTPS_PORT); } + public boolean isHTTPSConfigured() { + return getSslPort() != null; + } + public String getHttpsHost() { return getProperty(WEB_HTTPS_HOST); } diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml index 2df5caf..e03c69d 100644 --- a/nifi-registry-core/nifi-registry-web-api/pom.xml +++ b/nifi-registry-core/nifi-registry-web-api/pom.xml @@ -445,5 +445,11 @@ <version>${jetty.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.nifi.registry</groupId> + <artifactId>nifi-registry-jetty</artifactId> + <version>1.0.0-SNAPSHOT</version> + <scope>compile</scope> + </dependency> </dependencies> </project> diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java index 20a6e0d..b792830 100644 --- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java +++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java @@ -100,6 +100,12 @@ public class NiFiRegistrySecurityConfig extends WebSecurityConfigurerAdapter { .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + // Apply security headers for registry API. Security headers for docs and UI are applied with Jetty filters in registry-core. + http.headers().xssProtection(); + http.headers().contentSecurityPolicy("frame-ancestors 'self'"); + http.headers().httpStrictTransportSecurity().maxAgeInSeconds(31540000); + http.headers().frameOptions().sameOrigin(); + // x509 http.addFilterBefore(x509AuthenticationFilter(), AnonymousAuthenticationFilter.class);
