You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/12/17 22:32:32 UTC

[nifi] branch master updated: NIFI-5748 Improved Proxy Header Support

This is an automated email from the ASF dual-hosted git repository.

kdoran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/master by this push:
     new cc47a8c  NIFI-5748 Improved Proxy Header Support
cc47a8c is described below

commit cc47a8c0e1762b17f3889bf8e36b92cf51a8e7af
Author: Jeff Storck <jt...@gmail.com>
AuthorDate: Mon Oct 29 13:29:28 2018 -0400

    NIFI-5748 Improved Proxy Header Support
    
    - Fixed proxy header support to use X-Forwarded-Host instead of X-ForwardedServer
    - Added support for the context path header used by Traefik when proxying a service (X-Forwarded-Prefix)
    - Added tests to ApplicationResourceTest for X-Forwarded-Context and X-Forwarded-Prefix
    - Updated administration doc to include X-Forwarded-Prefix
    - Added NIFI_WEB_PROXY_CONTEXT_PATH env var to dockerhub and dockermaven start.sh scripts
    - Added documentation for NIFI_WEB_PROXY_CONTEXT_PATH to dockerhub README.md
    - Updated ApplicationResource to handle a port specified in X-ProxyPort and X-Forwarded-Port headers
    
    This closes #3129.
    
    Signed-off-by: Kevin Doran <kd...@apache.org>
---
 .../java/org/apache/nifi/web/util/WebUtils.java    |  19 ++--
 .../org/apache/nifi/web/util/WebUtilsTest.groovy   |  43 ++++++---
 nifi-docker/dockerhub/README.md                    |   5 +-
 nifi-docker/dockerhub/sh/start.sh                  |   1 +
 nifi-docker/dockermaven/sh/start.sh                |   1 +
 .../src/main/asciidoc/administration-guide.adoc    |   6 +-
 .../apache/nifi/web/api/ApplicationResource.java   |  50 +++++++++-
 .../nifi/web/api/ApplicationResourceTest.groovy    | 101 +++++++++++++++++++--
 .../apache/nifi/web/ContentViewerController.java   |   3 +-
 9 files changed, 195 insertions(+), 34 deletions(-)

diff --git a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
index 90a83a9..fbf5c19 100644
--- a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
+++ b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
@@ -21,6 +21,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Stream;
 import javax.net.ssl.SSLContext;
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpServletRequest;
@@ -45,6 +46,7 @@ public final class WebUtils {
 
     private static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath";
     private static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
+    private static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix";
 
     private WebUtils() {
     }
@@ -199,7 +201,8 @@ public final class WebUtils {
     }
 
     /**
-     * Determines the context path if populated in {@code X-ProxyContextPath} or {@code X-ForwardContext} headers. If not populated, returns an empty string.
+     * Determines the context path if populated in {@code X-ProxyContextPath}, {@code X-ForwardContext},
+     * or {@code X-Forwarded-Prefix} headers.  If not populated, returns an empty string.
      *
      * @param request the HTTP request
      * @return the provided context path or an empty string
@@ -208,18 +211,20 @@ public final class WebUtils {
         String contextPath = request.getContextPath();
         String proxyContextPath = request.getHeader(PROXY_CONTEXT_PATH_HTTP_HEADER);
         String forwardedContext = request.getHeader(FORWARDED_CONTEXT_HTTP_HEADER);
+        String prefix = request.getHeader(FORWARDED_PREFIX_HTTP_HEADER);
 
         logger.debug("Context path: " + contextPath);
         String determinedContextPath = "";
 
-        // If either header is set, log both
-        if (anyNotBlank(proxyContextPath, forwardedContext)) {
+        // If a context path header is set, log each
+        if (anyNotBlank(proxyContextPath, forwardedContext, prefix)) {
             logger.debug(String.format("On the request, the following context paths were parsed" +
-                            " from headers:\n\t X-ProxyContextPath: %s\n\tX-Forwarded-Context: %s",
-                    proxyContextPath, forwardedContext));
+                            " from headers:\n\t X-ProxyContextPath: %s\n\tX-Forwarded-Context: %s\n\tX-Forwarded-Prefix: %s",
+                    proxyContextPath, forwardedContext, prefix));
 
-            // Implementing preferred order here: PCP, FCP
-            determinedContextPath = StringUtils.isNotBlank(proxyContextPath) ? proxyContextPath : forwardedContext;
+            // Implementing preferred order here: PCP, FC, FP
+            determinedContextPath = Stream.of(proxyContextPath, forwardedContext, prefix)
+                    .filter(StringUtils::isNotBlank).findFirst().orElse("");
         }
 
         logger.debug("Determined context path: " + determinedContextPath);
diff --git a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy
index b0a1191..6b68457 100644
--- a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy
+++ b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsTest.groovy
@@ -32,10 +32,10 @@ import sun.security.x509.X500Name
 import javax.net.ssl.SSLPeerUnverifiedException
 import javax.servlet.http.HttpServletRequest
 import javax.ws.rs.core.UriBuilderException
-import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Client
 import javax.net.ssl.SSLContext
-import javax.net.ssl.HostnameVerifier;
-import java.security.cert.X509Certificate;
+import javax.net.ssl.HostnameVerifier
+import java.security.cert.X509Certificate
 
 
 @RunWith(JUnit4.class)
@@ -44,9 +44,10 @@ class WebUtilsTest extends GroovyTestCase {
 
     static final String PCP_HEADER = "X-ProxyContextPath"
     static final String FC_HEADER = "X-Forwarded-Context"
+    static final String FP_HEADER = "X-Forwarded-Prefix"
 
     static final String WHITELISTED_PATH = "/some/context/path"
-    private static final String OCSP_REQUEST_CONTENT_TYPE = "application/ocsp-request";
+    private static final String OCSP_REQUEST_CONTENT_TYPE = "application/ocsp-request"
 
     @BeforeClass
     static void setUpOnce() throws Exception {
@@ -78,6 +79,9 @@ class WebUtilsTest extends GroovyTestCase {
                         case FC_HEADER:
                             return keys["forward"]
                             break
+                        case FP_HEADER:
+                            return keys["prefix"]
+                            break
                         default:
                             return ""
                     }
@@ -94,8 +98,12 @@ class WebUtilsTest extends GroovyTestCase {
         // Variety of requests with different ordering of context paths (the correct one is always "some/context/path"
         HttpServletRequest proxyRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH])
         HttpServletRequest forwardedRequest = mockRequest([forward: CORRECT_CONTEXT_PATH])
+        HttpServletRequest prefixRequest = mockRequest([prefix: CORRECT_CONTEXT_PATH])
         HttpServletRequest proxyBeforeForwardedRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH, forward: WRONG_CONTEXT_PATH])
-        List<HttpServletRequest> requests = [proxyRequest, forwardedRequest, proxyBeforeForwardedRequest]
+        HttpServletRequest proxyBeforePrefixRequest = mockRequest([proxy: CORRECT_CONTEXT_PATH, prefix: WRONG_CONTEXT_PATH])
+        HttpServletRequest forwardBeforePrefixRequest = mockRequest([forward: CORRECT_CONTEXT_PATH, prefix: WRONG_CONTEXT_PATH])
+        List<HttpServletRequest> requests = [proxyRequest, forwardedRequest, prefixRequest, proxyBeforeForwardedRequest,
+                                             proxyBeforePrefixRequest, forwardBeforePrefixRequest]
 
         // Act
         requests.each { HttpServletRequest request ->
@@ -117,8 +125,12 @@ class WebUtilsTest extends GroovyTestCase {
         HttpServletRequest proxySpacesRequest = mockRequest([proxy: "   "])
         HttpServletRequest forwardedRequest = mockRequest([forward: ""])
         HttpServletRequest forwardedSpacesRequest = mockRequest([forward: "   "])
-        HttpServletRequest proxyBeforeForwardedRequest = mockRequest([proxy: "", forward: ""])
-        List<HttpServletRequest> requests = [proxyRequest, proxySpacesRequest, forwardedRequest, forwardedSpacesRequest, proxyBeforeForwardedRequest]
+        HttpServletRequest prefixRequest = mockRequest([prefix: ""])
+        HttpServletRequest prefixSpacesRequest = mockRequest([prefix: "   "])
+        HttpServletRequest proxyBeforeForwardedOrPrefixRequest = mockRequest([proxy: "", forward: "", prefix: ""])
+        HttpServletRequest proxyBeforeForwardedOrPrefixSpacesRequest = mockRequest([proxy: "   ", forward: "   ", prefix: "   "])
+        List<HttpServletRequest> requests = [proxyRequest, proxySpacesRequest, forwardedRequest, forwardedSpacesRequest, prefixRequest, prefixSpacesRequest,
+                                             proxyBeforeForwardedOrPrefixRequest, proxyBeforeForwardedOrPrefixSpacesRequest]
 
         // Act
         requests.each { HttpServletRequest request ->
@@ -156,7 +168,9 @@ class WebUtilsTest extends GroovyTestCase {
 
         HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "any/context/path"])
         HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "any/context/path", forward: "any/other/context/path"])
-        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithProxyAndForwardHeader]
+        HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy : "any/context/path", forward: "any/other/context/path",
+                                                                                    prefix: "any/other/prefix/path"])
+        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithProxyAndForwardHeader, requestWithProxyAndForwardAndPrefixHeader]
 
         // Act
         requests.each { HttpServletRequest request ->
@@ -179,7 +193,10 @@ class WebUtilsTest extends GroovyTestCase {
         HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"])
         HttpServletRequest requestWithForwardHeader = mockRequest([forward: "some/context/path"])
         HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path"])
-        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader]
+        HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path",
+                                                                                    prefix: "any/other/prefix/path"])
+        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader,
+                                             requestWithProxyAndForwardAndPrefixHeader]
 
         // Act
         requests.each { HttpServletRequest request ->
@@ -194,15 +211,19 @@ class WebUtilsTest extends GroovyTestCase {
     @Test
     void testGetResourcePathShouldAllowContextPathHeaderIfElementInMultipleWhitelist() throws Exception {
         // Arrange
-        String multipleWhitelistedPaths = [WHITELISTED_PATH, "/another/path", "/a/third/path"].join(",")
+        String multipleWhitelistedPaths = [WHITELISTED_PATH, "/another/path", "/a/third/path", "/a/prefix/path"].join(",")
         logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}")
 
         final List<String> VALID_RESOURCE_PATHS = multipleWhitelistedPaths.split(",").collect { "$it/actualResource" }
 
         HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"])
         HttpServletRequest requestWithForwardHeader = mockRequest([forward: "another/path"])
+        HttpServletRequest requestWithPrefixHeader = mockRequest([prefix: "a/prefix/path"])
         HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "a/third/path", forward: "any/other/context/path"])
-        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader]
+        HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy : "a/third/path", forward: "any/other/context/path",
+                                                                                    prefix: "any/other/prefix/path"])
+        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader,
+                                             requestWithPrefixHeader, requestWithProxyAndForwardAndPrefixHeader]
 
         // Act
         requests.each { HttpServletRequest request ->
diff --git a/nifi-docker/dockerhub/README.md b/nifi-docker/dockerhub/README.md
index e2123da..70aefee 100644
--- a/nifi-docker/dockerhub/README.md
+++ b/nifi-docker/dockerhub/README.md
@@ -191,6 +191,9 @@ can be published to the host.
 
 The Variable Registry can be configured for the docker image using the `NIFI_VARIABLE_REGISTRY_PROPERTIES` environment variable.
 
-=======
+=======  
+**NOTE**: If NiFi is proxied at context paths other than the root path of the proxy, the paths need to be set in the 
+_nifi.web.proxy.context.path_ property, which can be assigned via the environment variable _NIFI\_WEB\_PROXY\_CONTEXT\_PATH_.
+
 **NOTE**: If mapping the HTTPS port specifying trusted hosts should be provided for the property _nifi.web.proxy.host_.  This property can be specified to running instances
 via specifying an environment variable at container instantiation of _NIFI\_WEB\_PROXY\_HOST_.
diff --git a/nifi-docker/dockerhub/sh/start.sh b/nifi-docker/dockerhub/sh/start.sh
index 1cf5a7c..447da40 100755
--- a/nifi-docker/dockerhub/sh/start.sh
+++ b/nifi-docker/dockerhub/sh/start.sh
@@ -40,6 +40,7 @@ prop_replace 'nifi.zookeeper.connect.string'                "${NIFI_ZK_CONNECT_S
 prop_replace 'nifi.zookeeper.root.node'                     "${NIFI_ZK_ROOT_NODE:-/nifi}"
 prop_replace 'nifi.cluster.flow.election.max.wait.time'     "${NIFI_ELECTION_MAX_WAIT:-5 mins}"
 prop_replace 'nifi.cluster.flow.election.max.candidates'    "${NIFI_ELECTION_MAX_CANDIDATES:-}"
+prop_replace 'nifi.web.proxy.context.path'                  "${NIFI_WEB_PROXY_CONTEXT_PATH:-}"
 
 . "${scripts_dir}/update_cluster_state_management.sh"
 
diff --git a/nifi-docker/dockermaven/sh/start.sh b/nifi-docker/dockermaven/sh/start.sh
index 1cf5a7c..447da40 100755
--- a/nifi-docker/dockermaven/sh/start.sh
+++ b/nifi-docker/dockermaven/sh/start.sh
@@ -40,6 +40,7 @@ prop_replace 'nifi.zookeeper.connect.string'                "${NIFI_ZK_CONNECT_S
 prop_replace 'nifi.zookeeper.root.node'                     "${NIFI_ZK_ROOT_NODE:-/nifi}"
 prop_replace 'nifi.cluster.flow.election.max.wait.time'     "${NIFI_ELECTION_MAX_WAIT:-5 mins}"
 prop_replace 'nifi.cluster.flow.election.max.candidates'    "${NIFI_ELECTION_MAX_CANDIDATES:-}"
+prop_replace 'nifi.web.proxy.context.path'                  "${NIFI_WEB_PROXY_CONTEXT_PATH:-}"
 
 . "${scripts_dir}/update_cluster_state_management.sh"
 
diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index cded61e..61316f4 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -2292,8 +2292,8 @@ host[:port] the expected values need to be configured. This may be required when
 separated list in _nifi.properties_ using the `nifi.web.proxy.host` property (e.g. `localhost:18443, proxyhost:443`). IPv6 addresses are accepted. Please refer to
 RFC 5952 Sections link:https://tools.ietf.org/html/rfc5952#section-4[4] and link:https://tools.ietf.org/html/rfc5952#section-6[6] for additional details.
 
-** NiFi will only accept HTTP requests with a X-ProxyContextPath or X-Forwarded-Context header if the value is whitelisted in the `nifi.web.proxy.context.path` property in
-_nifi.properties_. This property accepts a comma separated list of expected values. In the event an incoming request has an X-ProxyContextPath or X-Forwarded-Context header value that is not
+** NiFi will only accept HTTP requests with a X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header if the value is whitelisted in the `nifi.web.proxy.context.path` property in
+_nifi.properties_. This property accepts a comma separated list of expected values. In the event an incoming request has an X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header value that is not
 present in the whitelist, the "An unexpected error has occurred" page will be shown and an error will be written to the _nifi-app.log_.
 
 * Additional configurations at both proxy server and NiFi cluster are required to make NiFi Site-to-Site work behind reverse proxies. See <<site_to_site_reverse_proxy_properties>> for details.
@@ -3009,7 +3009,7 @@ Providing three total network interfaces, including  `nifi.web.https.network.int
 |`nifi.web.proxy.host`|A comma separated list of allowed HTTP Host header values to consider when NiFi is running securely and will be receiving requests to a different host[:port] than it is bound to.
 For example, when running in a Docker container or behind a proxy (e.g. localhost:18443, proxyhost:443). By default, this value is blank meaning NiFi should only allow requests sent to the
 host[:port] that NiFi is bound to.
-|`nifi.web.proxy.context.path`|A comma separated list of allowed HTTP X-ProxyContextPath or X-Forwarded-Context header values to consider. By default, this value is
+|`nifi.web.proxy.context.path`|A comma separated list of allowed HTTP X-ProxyContextPath, X-Forwarded-Context, or X-Forwarded-Prefix header values to consider. By default, this value is
 blank meaning all requests containing a proxy context path are rejected. Configuring this property would allow requests where the proxy path is contained in this listing.
 |====
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index d99fb96..f675a46 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -107,10 +107,13 @@ public abstract class ApplicationResource {
     public static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath";
 
     public static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto";
-    public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Server";
+    public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Host";
     public static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port";
     public static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
 
+    // Traefik-specific headers
+    public static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix";
+
     protected static final String NON_GUARANTEED_ENDPOINT = "Note: This endpoint is subject to change as NiFi and it's REST API evolve.";
 
     private static final Logger logger = LoggerFactory.getLogger(ApplicationResource.class);
@@ -151,8 +154,11 @@ public abstract class ApplicationResource {
             // check for proxy settings
 
             final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER);
-            final String host = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
-            final String port = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER);
+            final String hostHeaderValue = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
+            final String portHeaderValue = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER);
+
+            final String host = determineProxiedHost(hostHeaderValue);
+            final String port = determineProxiedPort(hostHeaderValue, portHeaderValue);
 
             // Catch header poisoning
             String whitelistedContextPaths = properties.getWhitelistedContextPaths();
@@ -188,6 +194,44 @@ public abstract class ApplicationResource {
         return uri;
     }
 
+    private String determineProxiedHost(String hostHeaderValue) {
+        final String host;
+        // check for a port in the proxied host header
+        String[] hostSplits = hostHeaderValue == null ? new String[] {} : hostHeaderValue.split(":");
+        if (hostSplits.length >= 1 && hostSplits.length <= 2) {
+            // zero or one occurrence of ':', this is an IPv4 address
+            // strip off the port by reassigning host the 0th split
+            host = hostSplits[0];
+        } else if (hostSplits.length == 0) {
+            // hostHeaderValue passed in was null, no splits
+            host = null;
+        } else {
+            // hostHeaderValue has more than one occurrence of ":", IPv6 address
+            host = hostHeaderValue;
+        }
+        return host;
+    }
+
+    private String determineProxiedPort(String hostHeaderValue, String portHeaderValue) {
+        final String port;
+        // check for a port in the proxied host header
+        String[] hostSplits = hostHeaderValue == null ? new String[] {} : hostHeaderValue.split(":");
+        // determine the proxied port
+        final String portFromHostHeader;
+        if (hostSplits.length == 2) {
+            // if the port is specified in the proxied host header, it will be overridden by the
+            // port specified in X-ProxyPort or X-Forwarded-Port
+            portFromHostHeader = hostSplits[1];
+        } else {
+            portFromHostHeader = null;
+        }
+        if (StringUtils.isNotBlank(portFromHostHeader) && StringUtils.isNotBlank(portHeaderValue)) {
+            logger.warn(String.format("The proxied host header contained a port, but was overridden by the proxied port header"));
+        }
+        port = StringUtils.isNotBlank(portHeaderValue) ? portHeaderValue : (StringUtils.isNotBlank(portFromHostHeader) ? portFromHostHeader : null);
+        return port;
+    }
+
     /**
      * Edit the response headers to indicating no caching.
      *
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
index 7e773c1..00e888c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
@@ -36,6 +36,9 @@ import javax.ws.rs.core.UriInfo
 class ApplicationResourceTest extends GroovyTestCase {
     private static final Logger logger = LoggerFactory.getLogger(ApplicationResourceTest.class)
 
+    public static final String PROXY_HOST_HTTP_HEADER = "X-ProxyHost"
+    public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Host"
+
     static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme"
     static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort"
     static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath"
@@ -43,6 +46,7 @@ class ApplicationResourceTest extends GroovyTestCase {
     static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto"
     static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port"
     static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context"
+    static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix"
 
     static final String PROXY_CONTEXT_PATH_PROP = NiFiProperties.WEB_PROXY_CONTEXT_PATH
     static final String WHITELISTED_PATH = "/some/context/path"
@@ -73,21 +77,29 @@ class ApplicationResourceTest extends GroovyTestCase {
     }
 
     private ApplicationResource buildApplicationResource() {
+        buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER, PROXY_CONTEXT_PATH_HTTP_HEADER])
+    }
+
+    private ApplicationResource buildApplicationResource(List proxyHeaders) {
         ApplicationResource resource = new MockApplicationResource()
+        String headerValue = ""
         HttpServletRequest mockRequest = [getHeader: { String k ->
-            logger.mock("Request.getHeader($k)")
-            if ([FORWARDED_CONTEXT_HTTP_HEADER, PROXY_CONTEXT_PATH_HTTP_HEADER].contains(k)) {
-                WHITELISTED_PATH
+            if (proxyHeaders.contains(k)) {
+                headerValue = WHITELISTED_PATH
             } else if ([FORWARDED_PORT_HTTP_HEADER, PROXY_PORT_HTTP_HEADER].contains(k)) {
-                "8081"
+                headerValue = "8081"
             } else if ([FORWARDED_PROTO_HTTP_HEADER, PROXY_SCHEME_HTTP_HEADER].contains(k)) {
-                "https"
+                headerValue = "https"
+            } else if ([PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER].contains(k)) {
+                headerValue = "nifi.apache.org:8081"
             } else {
-                "nifi.apache.org"
+                headerValue = ""
             }
+            logger.mock("Request.getHeader($k) -> \"$headerValue\"")
+            headerValue
         }, getContextPath: { ->
-            logger.mock("Request.getContextPath()")
-            ""
+            logger.mock("Request.getContextPath() -> \"$headerValue\"")
+            headerValue
         }] as HttpServletRequest
 
         UriInfo mockUriInfo = [getBaseUriBuilder: { ->
@@ -155,27 +167,100 @@ class ApplicationResourceTest extends GroovyTestCase {
     @Test
     void testGenerateUriShouldBlockForwardedContextHeaderIfNotInWhitelist() throws Exception {
         // Arrange
+        ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
+        logger.info("Whitelisted path(s): ")
+
+        // Act
+        def msg = shouldFail(UriBuilderException) {
+            String generatedUri = resource.generateResourceUri('actualResource')
+            logger.unexpected("Generated URI: ${generatedUri}")
+        }
+
+        // Assert
+        logger.expected(msg)
+        assert msg =~ "The provided context path \\[.*\\] was not whitelisted \\[\\]"
+    }
+
+    @Test
+    void testGenerateUriShouldBlockForwardedPrefixHeaderIfNotInWhitelist() throws Exception {
+        // Arrange
+        ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
+        logger.info("Whitelisted path(s): ")
 
         // Act
+        def msg = shouldFail(UriBuilderException) {
+            String generatedUri = resource.generateResourceUri('actualResource')
+            logger.unexpected("Generated URI: ${generatedUri}")
+        }
 
         // Assert
+        logger.expected(msg)
+        assert msg =~ "The provided context path \\[.*\\] was not whitelisted \\[\\]"
     }
 
     @Test
     void testGenerateUriShouldAllowForwardedContextHeaderIfInWhitelist() throws Exception {
         // Arrange
+        ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
+        logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
+        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): WHITELISTED_PATH] as Properties)
+        resource.properties = niFiProperties
 
         // Act
+        String generatedUri = resource.generateResourceUri('actualResource')
+        logger.info("Generated URI: ${generatedUri}")
 
         // Assert
+        assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
+    }
+
+    @Test
+    void testGenerateUriShouldAllowForwardedPrefixHeaderIfInWhitelist() throws Exception {
+        // Arrange
+        ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
+        logger.info("Whitelisted path(s): ${WHITELISTED_PATH}")
+        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): WHITELISTED_PATH] as Properties)
+        resource.properties = niFiProperties
+
+        // Act
+        String generatedUri = resource.generateResourceUri('actualResource')
+        logger.info("Generated URI: ${generatedUri}")
+
+        // Assert
+        assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
     }
 
     @Test
     void testGenerateUriShouldAllowForwardedContextHeaderIfElementInMultipleWhitelist() throws Exception {
         // Arrange
+        ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
+        String multipleWhitelistedPaths = [WHITELISTED_PATH, "another/path", "a/third/path"].join(",")
+        logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}")
+        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleWhitelistedPaths] as Properties)
+        resource.properties = niFiProperties
 
         // Act
+        String generatedUri = resource.generateResourceUri('actualResource')
+        logger.info("Generated URI: ${generatedUri}")
 
         // Assert
+        assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
+    }
+
+    @Test
+    void testGenerateUriShouldAllowForwardedPrefixHeaderIfElementInMultipleWhitelist() throws Exception {
+        // Arrange
+        ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
+        String multipleWhitelistedPaths = [WHITELISTED_PATH, "another/path", "a/third/path"].join(",")
+        logger.info("Whitelisted path(s): ${multipleWhitelistedPaths}")
+        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleWhitelistedPaths] as Properties)
+        resource.properties = niFiProperties
+
+        // Act
+        String generatedUri = resource.generateResourceUri('actualResource')
+        logger.info("Generated URI: ${generatedUri}")
+
+        // Assert
+        assert generatedUri == "https://nifi.apache.org:8081${WHITELISTED_PATH}/actualResource"
     }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
index 8ac0179..b4638c9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-content-viewer/src/main/java/org/apache/nifi/web/ContentViewerController.java
@@ -57,6 +57,7 @@ public class ContentViewerController extends HttpServlet {
 
     private static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath";
     private static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
+    private static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix";
 
   /**
      * Gets the content and defers to registered viewers to generate the markup.
@@ -311,7 +312,7 @@ public class ContentViewerController extends HttpServlet {
         refUriBuilder.scheme(request.getScheme());
 
         // If there is path context from a proxy, remove it since this request will be used inside the cluster
-        final String proxyContextPath = getFirstHeaderValue(request, PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER);
+        final String proxyContextPath = getFirstHeaderValue(request, PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER, FORWARDED_PREFIX_HTTP_HEADER);
         if (StringUtils.isNotBlank(proxyContextPath)) {
             refUriBuilder.replacePath(StringUtils.substringAfter(UriBuilder.fromUri(ref).build().getPath(), proxyContextPath));
         }