Skip to content

Commit

Permalink
NIFI-7558 Fixed CatchAllFilter init logic by calling super.init().
Browse files Browse the repository at this point in the history
Renamed legacy terms.
Updated documentation.

This closes apache#4351.

Signed-off-by: Mark Payne <markap14@hotmail.com>
  • Loading branch information
alopresto authored and agentdalecoper committed Jun 26, 2020
1 parent 0353022 commit 7eba318
Show file tree
Hide file tree
Showing 14 changed files with 490 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1559,23 +1559,23 @@ public Map<String, String> getContentRepositoryEncryptionKeys() {
}

/**
* Returns the whitelisted proxy hostnames (and IP addresses) as a comma-delimited string.
* Returns the allowed proxy hostnames (and IP addresses) as a comma-delimited string.
* The hosts have been normalized to the form {@code somehost.com}, {@code somehost.com:port}, or {@code 127.0.0.1}.
* <p>
* Note: Calling {@code NiFiProperties.getProperty(NiFiProperties.WEB_PROXY_HOST)} will not normalize the hosts.
*
* @return the hostname(s)
*/
public String getWhitelistedHosts() {
return StringUtils.join(getWhitelistedHostsAsList(), ",");
public String getAllowedHosts() {
return StringUtils.join(getAllowedHostsAsList(), ",");
}

/**
* Returns the whitelisted proxy hostnames (and IP addresses) as a List. The hosts have been normalized to the form {@code somehost.com}, {@code somehost.com:port}, or {@code 127.0.0.1}.
* Returns the allowed proxy hostnames (and IP addresses) as a List. The hosts have been normalized to the form {@code somehost.com}, {@code somehost.com:port}, or {@code 127.0.0.1}.
*
* @return the hostname(s)
*/
public List<String> getWhitelistedHostsAsList() {
public List<String> getAllowedHostsAsList() {
String rawProperty = getProperty(WEB_PROXY_HOST, "");
List<String> hosts = Arrays.asList(rawProperty.split(","));
return hosts.stream()
Expand All @@ -1591,22 +1591,22 @@ String normalizeHost(String host) {
}

/**
* Returns the whitelisted proxy context paths as a comma-delimited string. The paths have been normalized to the form {@code /some/context/path}.
* Returns the allowed proxy context paths as a comma-delimited string. The paths have been normalized to the form {@code /some/context/path}.
* <p>
* Note: Calling {@code NiFiProperties.getProperty(NiFiProperties.WEB_PROXY_CONTEXT_PATH)} will not normalize the paths.
*
* @return the path(s)
*/
public String getWhitelistedContextPaths() {
return StringUtils.join(getWhitelistedContextPathsAsList(), ",");
public String getAllowedContextPaths() {
return StringUtils.join(getAllowedContextPathsAsList(), ",");
}

/**
* Returns the whitelisted proxy context paths as a list of paths. The paths have been normalized to the form {@code /some/context/path}.
* Returns the allowed proxy context paths as a list of paths. The paths have been normalized to the form {@code /some/context/path}.
*
* @return the path(s)
*/
public List<String> getWhitelistedContextPathsAsList() {
public List<String> getAllowedContextPathsAsList() {
String rawProperty = getProperty(WEB_PROXY_CONTEXT_PATH, "");
List<String> contextPaths = Arrays.asList(rawProperty.split(","));
return contextPaths.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.nifi.util.StringUtils;
import org.apache.nifi.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -32,16 +33,17 @@
*/
public class SanitizeContextPathFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(SanitizeContextPathFilter.class);
private static final String ALLOWED_CONTEXT_PATHS_PARAMETER_NAME = "allowedContextPaths";

private String whitelistedContextPaths = "";

private String allowedContextPaths = "";

@Override
public void init(FilterConfig filterConfig) throws ServletException {
String providedWhitelist = filterConfig.getServletContext().getInitParameter("whitelistedContextPaths");
logger.debug("SanitizeContextPathFilter received provided whitelisted context paths from NiFi properties: " + providedWhitelist);
if (providedWhitelist != null) {
whitelistedContextPaths = providedWhitelist;
String providedAllowedList = filterConfig.getServletContext().getInitParameter(ALLOWED_CONTEXT_PATHS_PARAMETER_NAME);

logger.debug("SanitizeContextPathFilter received provided allowed context paths from NiFi properties: " + providedAllowedList);
if (StringUtils.isNotBlank(providedAllowedList)) {
allowedContextPaths = providedAllowedList;
}
}

Expand All @@ -56,11 +58,12 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha

/**
* Determines, sanitizes, and injects the {@code contextPath} attribute into the {@code request}. If not present, an empty string {@code ""} is injected.
*
* @param request the request
*/
protected void injectContextPathAttribute(ServletRequest request) {
// Capture the provided context path headers and sanitize them before using in the response
String contextPath = WebUtils.sanitizeContextPath(request, whitelistedContextPaths, "");
String contextPath = WebUtils.sanitizeContextPath(request, allowedContextPaths, "");
request.setAttribute("contextPath", contextPath);

logger.debug("SanitizeContextPathFilter set contextPath: " + contextPath);
Expand All @@ -71,11 +74,11 @@ public void destroy() {
}

/**
* Getter for whitelistedContextPaths. Cannot be package-private because of an issue where the package is scoped per classloader.
* Getter for allowed context paths. Cannot be package-private because of an issue where the package is scoped per classloader.
*
* @return the whitelisted context path(s)
* @return the allowed context path(s)
*/
protected String getWhitelistedContextPaths() {
return whitelistedContextPaths;
protected String getAllowedContextPaths() {
return allowedContextPaths;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.UriBuilderException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;

/**
* Common utilities related to web development.
*/
public final class WebUtils {

private static Logger logger = LoggerFactory.getLogger(WebUtils.class);
private static final Logger logger = LoggerFactory.getLogger(WebUtils.class);

final static ReadWriteLock lock = new ReentrantReadWriteLock();

Expand Down Expand Up @@ -107,15 +107,15 @@ private static Client createClientHelper(final ClientConfig config, final SSLCon
}

/**
* This method will check the provided context path headers against a whitelist (provided in nifi.properties) and throw an exception if the requested context path is not registered.
* This method will check the provided context path headers against an allow list (provided in nifi.properties) and throw an exception if the requested context path is not registered.
*
* @param uri the request URI
* @param request the HTTP request
* @param whitelistedContextPaths comma-separated list of valid context paths
* @param allowedContextPaths comma-separated list of valid context paths
* @return the resource path
* @throws UriBuilderException if the requested context path is not registered (header poisoning)
*/
public static String getResourcePath(URI uri, HttpServletRequest request, String whitelistedContextPaths) throws UriBuilderException {
public static String getResourcePath(URI uri, HttpServletRequest request, String allowedContextPaths) throws UriBuilderException {
String resourcePath = uri.getPath();

// Determine and normalize the context path
Expand All @@ -124,7 +124,7 @@ public static String getResourcePath(URI uri, HttpServletRequest request, String

// If present, check it and prepend to the resource path
if (StringUtils.isNotBlank(determinedContextPath)) {
verifyContextPath(whitelistedContextPaths, determinedContextPath);
verifyContextPath(allowedContextPaths, determinedContextPath);

// Determine the complete resource path
resourcePath = determinedContextPath + resourcePath;
Expand All @@ -134,22 +134,22 @@ public static String getResourcePath(URI uri, HttpServletRequest request, String
}

/**
* Throws an exception if the provided context path is not in the whitelisted context paths list.
* Throws an exception if the provided context path is not in the allowed context paths list.
*
* @param whitelistedContextPaths a comma-delimited list of valid context paths
* @param allowedContextPaths a comma-delimited list of valid context paths
* @param determinedContextPath the normalized context path from a header
* @throws UriBuilderException if the context path is not safe
*/
public static void verifyContextPath(String whitelistedContextPaths, String determinedContextPath) throws UriBuilderException {
public static void verifyContextPath(String allowedContextPaths, String determinedContextPath) throws UriBuilderException {
// If blank, ignore
if (StringUtils.isBlank(determinedContextPath)) {
return;
}

// Check it against the whitelist
List<String> individualContextPaths = Arrays.asList(StringUtils.split(whitelistedContextPaths, ","));
// Check it against the allowed list
List<String> individualContextPaths = Arrays.asList(StringUtils.split(allowedContextPaths, ","));
if (!individualContextPaths.contains(determinedContextPath)) {
final String msg = "The provided context path [" + determinedContextPath + "] was not whitelisted [" + whitelistedContextPaths + "]";
final String msg = "The provided context path [" + determinedContextPath + "] was not registered as allowed [" + allowedContextPaths + "]";
logger.error(msg);
throw new UriBuilderException(msg);
}
Expand Down Expand Up @@ -184,15 +184,17 @@ public static String normalizeContextPath(String determinedContextPath) {
* If no headers are present specifying this value, it is an empty string.
*
* @param request the HTTP request
* @param allowedContextPaths the comma-separated list of allowed context paths
* @param jspDisplayName the display name of the resource for log messages
* @return the context path safe to be printed to the page
*/
public static String sanitizeContextPath(ServletRequest request, String whitelistedContextPaths, String jspDisplayName) {
public static String sanitizeContextPath(ServletRequest request, String allowedContextPaths, String jspDisplayName) {
if (StringUtils.isBlank(jspDisplayName)) {
jspDisplayName = "JSP page";
}
String contextPath = normalizeContextPath(determineContextPath((HttpServletRequest) request));
try {
verifyContextPath(whitelistedContextPaths, contextPath);
verifyContextPath(allowedContextPaths, contextPath);
return contextPath;
} catch (UriBuilderException e) {
logger.error("Error determining context path on " + jspDisplayName + ": " + e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* 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.filter

import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import javax.servlet.FilterConfig
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
import java.security.Security

@RunWith(JUnit4.class)
class SanitizeContextPathFilterTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(SanitizeContextPathFilterTest.class)

@BeforeClass
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())

logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}

@Before
void setUp() throws Exception {

}

@After
void tearDown() throws Exception {

}

private static String getValue(String parameterName, Map<String, String> params = [:]) {
params.containsKey(parameterName) ? params[parameterName] : ""
}

@Test
void testInitShouldExtractAllowedContextPaths() {
// Arrange
def EXPECTED_ALLOWED_CONTEXT_PATHS = ["/path1", "/path2"].join(", ")
def parameters = [allowedContextPaths: EXPECTED_ALLOWED_CONTEXT_PATHS]
FilterConfig mockFilterConfig = [
getInitParameter : { String parameterName -> getValue(parameterName, parameters) },
getServletContext: { ->
[getInitParameter: { String parameterName -> return getValue(parameterName, parameters) }] as ServletContext
}] as FilterConfig

SanitizeContextPathFilter scpf = new SanitizeContextPathFilter()

// Act
scpf.init(mockFilterConfig)
logger.info("Allowed context paths: ${scpf.getAllowedContextPaths()}")

// Assert
assert scpf.getAllowedContextPaths() == EXPECTED_ALLOWED_CONTEXT_PATHS
}

@Test
void testInitShouldHandleBlankAllowedContextPaths() {
// Arrange
def EXPECTED_ALLOWED_CONTEXT_PATHS = ""
FilterConfig mockFilterConfig = [
getInitParameter : { String parameterName -> "" },
getServletContext: { ->
[getInitParameter: { String parameterName -> "" }] as ServletContext
}] as FilterConfig

SanitizeContextPathFilter scpf = new SanitizeContextPathFilter()

// Act
scpf.init(mockFilterConfig)
logger.info("Allowed context paths: ${scpf.getAllowedContextPaths()}")

// Assert
assert scpf.getAllowedContextPaths() == EXPECTED_ALLOWED_CONTEXT_PATHS
}

@Test
void testShouldInjectContextPathAttribute() {
// Arrange
final String EXPECTED_ALLOWED_CONTEXT_PATHS = ["/path1", "/path2"].join(", ")
final String EXPECTED_FORWARD_PATH = "index.jsp"
final Map PARAMETERS = [
allowedContextPaths: EXPECTED_ALLOWED_CONTEXT_PATHS,
forwardPath : EXPECTED_FORWARD_PATH
]

final String EXPECTED_CONTEXT_PATH = "/path1"

// Mock collaborators
FilterConfig mockFilterConfig = [
getInitParameter : { String parameterName ->
return getValue(parameterName, PARAMETERS)
},
getServletContext: { ->
[getInitParameter: { String parameterName ->
return getValue(parameterName, PARAMETERS)
}] as ServletContext
}] as FilterConfig

// Local map to store request attributes
def requestAttributes = [:]

final Map HEADERS = [
"X-ProxyContextPath" : "path1",
"X-Forwarded-Context": "",
"X-Forwarded-Prefix" : ""]

HttpServletRequest mockRequest = [
getContextPath : { -> EXPECTED_CONTEXT_PATH },
getHeader : { String headerName -> getValue(headerName, HEADERS) },
setAttribute : { String attr, String value ->
requestAttributes[attr] = value
logger.mock("Set request attribute ${attr} to ${value}")
},
] as HttpServletRequest

SanitizeContextPathFilter scpf = new SanitizeContextPathFilter()
scpf.init(mockFilterConfig)
logger.info("Allowed context paths: ${scpf.getAllowedContextPaths()}")

// Act
scpf.injectContextPathAttribute(mockRequest)

// Assert
assert requestAttributes["contextPath"] == EXPECTED_CONTEXT_PATH
}
}
Loading

0 comments on commit 7eba318

Please sign in to comment.