NIFI-9995 Replaced Custom Filters with Spring Security HeaderWriter

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #6020.
This commit is contained in:
exceptionfactory 2022-05-06 08:18:52 -05:00 committed by Nathan Gough
parent 64f9b66141
commit ea75a0a996
9 changed files with 31 additions and 478 deletions

View File

@ -20,18 +20,21 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.processor.DataUnit;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
import org.apache.nifi.web.security.requests.ContentLengthFilter;
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlets.DoSFilter;
import org.springframework.security.web.header.HeaderWriter;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.ContentSecurityPolicyHeaderWriter;
import org.springframework.security.web.header.writers.HstsHeaderWriter;
import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter;
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import javax.servlet.Filter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@ -42,6 +45,8 @@ import java.util.concurrent.TimeUnit;
public class StandardRequestFilterProvider implements RequestFilterProvider {
private static final int MAX_CONTENT_SIZE_DISABLED = 0;
private static final String STANDARD_CONTENT_POLICY = "frame-ancestors 'self'";
/**
* Get Filters using provided NiFi Properties
*
@ -54,13 +59,9 @@ public class StandardRequestFilterProvider implements RequestFilterProvider {
final List<FilterHolder> filters = new ArrayList<>();
filters.add(getFilterHolder(XFrameOptionsFilter.class));
filters.add(getFilterHolder(ContentSecurityPolicyFilter.class));
filters.add(getFilterHolder(XSSProtectionFilter.class));
filters.add(getFilterHolder(XContentTypeOptionsFilter.class));
filters.add(getHeaderWriterFilter());
if (properties.isHTTPSConfigured()) {
filters.add(getFilterHolder(StrictTransportSecurityFilter.class));
filters.add(getFilterHolder(RequestAuthenticationFilter.class));
}
@ -92,6 +93,21 @@ public class StandardRequestFilterProvider implements RequestFilterProvider {
return filter;
}
private FilterHolder getHeaderWriterFilter() {
final List<HeaderWriter> headerWriters = Arrays.asList(
new ContentSecurityPolicyHeaderWriter(STANDARD_CONTENT_POLICY),
new HstsHeaderWriter(),
new XContentTypeOptionsHeaderWriter(),
new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN),
new XXssProtectionHeaderWriter()
);
final HeaderWriterFilter headerWriterFilter = new HeaderWriterFilter(headerWriters);
final FilterHolder filterHolder = new FilterHolder(headerWriterFilter);
filterHolder.setName(HeaderWriterFilter.class.getSimpleName());
return filterHolder;
}
private FilterHolder getFilterHolder(final Class<? extends Filter> filterClass) {
final FilterHolder filter = new FilterHolder(filterClass);
filter.setName(filterClass.getSimpleName());
@ -99,7 +115,7 @@ public class StandardRequestFilterProvider implements RequestFilterProvider {
}
private FilterHolder getContentLengthFilter(final int maxContentSize) {
final FilterHolder filter = new FilterHolder(ContentLengthFilter.class);
final FilterHolder filter = getFilterHolder(ContentLengthFilter.class);
filter.setInitParameter(ContentLengthFilter.MAX_LENGTH_INIT_PARAM, Integer.toString(maxContentSize));
filter.setName(ContentLengthFilter.class.getSimpleName());
return filter;

View File

@ -17,14 +17,11 @@
package org.apache.nifi.web.server.filter;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlets.DoSFilter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.web.header.HeaderWriterFilter;
import javax.servlet.Filter;
import java.util.Collections;
@ -59,11 +56,8 @@ public class RestApiRequestFilterProviderTest {
assertNotNull(filters);
assertFalse(filters.isEmpty());
assertFilterClassFound(filters, HeaderWriterFilter.class);
assertFilterClassFound(filters, DataTransferExcludedDoSFilter.class);
assertFilterClassFound(filters, XFrameOptionsFilter.class);
assertFilterClassFound(filters, ContentSecurityPolicyFilter.class);
assertFilterClassFound(filters, XSSProtectionFilter.class);
assertFilterClassFound(filters, XContentTypeOptionsFilter.class);
}
private void assertFilterClassFound(final List<FilterHolder> filters, final Class<? extends Filter> filterClass) {

View File

@ -17,16 +17,12 @@
package org.apache.nifi.web.server.filter;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.apache.nifi.web.security.headers.StrictTransportSecurityFilter;
import org.apache.nifi.web.security.headers.XContentTypeOptionsFilter;
import org.apache.nifi.web.security.headers.XFrameOptionsFilter;
import org.apache.nifi.web.security.headers.XSSProtectionFilter;
import org.apache.nifi.web.security.requests.ContentLengthFilter;
import org.apache.nifi.web.server.log.RequestAuthenticationFilter;
import org.eclipse.jetty.servlet.FilterHolder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.web.header.HeaderWriterFilter;
import javax.servlet.Filter;
import java.util.Collections;
@ -83,18 +79,14 @@ public class StandardRequestFilterProviderTest {
assertStandardFiltersFound(filters);
assertFilterClassFound(filters, RequestAuthenticationFilter.class);
assertFilterClassFound(filters, StrictTransportSecurityFilter.class);
}
private void assertStandardFiltersFound(final List<FilterHolder> filters) {
assertNotNull(filters);
assertFalse(filters.isEmpty());
assertFilterClassFound(filters, HeaderWriterFilter.class);
assertFilterClassFound(filters, DataTransferExcludedDoSFilter.class);
assertFilterClassFound(filters, XFrameOptionsFilter.class);
assertFilterClassFound(filters, ContentSecurityPolicyFilter.class);
assertFilterClassFound(filters, XSSProtectionFilter.class);
assertFilterClassFound(filters, XContentTypeOptionsFilter.class);
}
private void assertFilterClassFound(final List<FilterHolder> filters, final Class<? extends Filter> filterClass) {

View File

@ -1,57 +0,0 @@
/*
* 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.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 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() {
}
}

View File

@ -1,58 +0,0 @@
/*
* 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() {
}
}

View File

@ -1,58 +0,0 @@
/*
* 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-Content-Type-Options header.
*
*/
public class XContentTypeOptionsFilter implements Filter {
private static final String HEADER = "X-Content-Type-Options";
private static final String POLICY = "nosniff";
private static final Logger logger = LoggerFactory.getLogger(XContentTypeOptionsFilter.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() {
}
}

View File

@ -1,58 +0,0 @@
/*
* 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() {
}
}

View File

@ -1,58 +0,0 @@
/*
* 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() {
}
}

View File

@ -1,160 +0,0 @@
/*
* 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.apache.nifi.web.security.headers.ContentSecurityPolicyFilter;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletHandler;
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 HTTPHeaderFiltersTest {
@Test
public void testCSPHeaderApplied() throws ServletException, IOException, Exception {
// Arrange
FilterHolder cspFilter = new FilterHolder(new ContentSecurityPolicyFilter());
// Set up request
HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
FilterChain mockFilterChain = Mockito.mock(FilterChain.class);
// Action
cspFilter.setServletHandler(new ServletHandler());
cspFilter.start();
cspFilter.initialize();
cspFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain);
// Verify
assertEquals("frame-ancestors 'self'", mockResponse.getHeader("Content-Security-Policy"));
}
@Test
public void testCSPHeaderAppliedOnlyOnce() throws ServletException, IOException, Exception {
// Arrange
FilterHolder cspFilter = new FilterHolder(new ContentSecurityPolicyFilter());
// Set up request
HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
FilterChain mockFilterChain = Mockito.mock(FilterChain.class);
// Action
cspFilter.setServletHandler(new ServletHandler());
cspFilter.start();
cspFilter.initialize();
cspFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain);
cspFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain);
// Verify
assertEquals("frame-ancestors 'self'", mockResponse.getHeader("Content-Security-Policy"));
}
@Test
public void testXFrameOptionsHeaderApplied() throws ServletException, IOException, Exception {
// 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.setServletHandler(new ServletHandler());
xfoFilter.start();
xfoFilter.initialize();
xfoFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain);
// Verify
assertEquals("SAMEORIGIN", mockResponse.getHeader("X-Frame-Options"));
}
@Test
public void testHSTSHeaderApplied() throws ServletException, IOException, Exception {
// 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.setServletHandler(new ServletHandler());
hstsFilter.start();
hstsFilter.initialize();
hstsFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain);
// Verify
assertEquals("max-age=31540000", mockResponse.getHeader("Strict-Transport-Security"));
}
@Test
public void testXSSProtectionHeaderApplied() throws ServletException, IOException, Exception {
// 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.setServletHandler(new ServletHandler());
xssFilter.start();
xssFilter.initialize();
xssFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain);
// Verify
assertEquals("1; mode=block", mockResponse.getHeader("X-XSS-Protection"));
}
@Test
public void testXContentTypeOptionsHeaderApplied() throws Exception {
// Arrange
FilterHolder xContentTypeFilter = new FilterHolder(new XContentTypeOptionsFilter());
HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class);
MockHttpServletResponse mockResponse = new MockHttpServletResponse();
FilterChain mockFilterChain = Mockito.mock(FilterChain.class);
// Action
xContentTypeFilter.setServletHandler(new ServletHandler());
xContentTypeFilter.start();
xContentTypeFilter.initialize();
xContentTypeFilter.getFilter().doFilter(mockRequest, mockResponse, mockFilterChain);
// Verify
assertEquals("nosniff", mockResponse.getHeader("X-Content-Type-Options"));
}
}