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

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


The following commit(s) were added to refs/heads/main by this push:
     new 41625c44338 NIFI-15953 Improved Proxy Host Header Documentation and 
Validation (#11268)
41625c44338 is described below

commit 41625c4433898922b0a3e7635f597cb4ef197c28
Author: David Handermann <[email protected]>
AuthorDate: Thu May 21 07:06:16 2026 -0500

    NIFI-15953 Improved Proxy Host Header Documentation and Validation (#11268)
---
 .../src/main/asciidoc/administration-guide.adoc    | 28 +++++--
 .../connector/FrameworkServerConnectorFactory.java | 23 +++++-
 .../connector/ProxyHeaderValidatorCustomizer.java  | 91 ++++++++++++++++++++++
 .../web/server/StandardServerProviderTest.java     | 39 +++++++++-
 4 files changed, 170 insertions(+), 11 deletions(-)

diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc 
b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index 0ef958ee612..d5e4234bc39 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -2758,10 +2758,16 @@ documentation of the proxy for guidance for your 
deployment environment and use
 
 * Additional NiFi proxy configuration must be updated to allow expected Host 
and context paths HTTP headers.
 
-** By default, if NiFi is running securely it will only accept HTTP requests 
with a Host header matching the host[:port] that it is bound to. If NiFi is to 
accept requests directed to a different
-host[:port] the expected values need to be configured. This may be required 
when running behind a proxy or in a containerized environment. This is 
configured in a comma
-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.
+** When configured with HTTPS enabled, the name in the `Host` header must 
match one of the DNS Subject Alternative Names
+included on the configured server X.509 certificate. The framework server also 
validates the name provided in the Server
+Name Indication extension during the TLS handshake against the configured 
server certificate. The server returns an HTTP
+400 Bad Request status when the requested `Host` header does not meet these 
requirements.
+
+** The application supports providing alternative host addresses through 
either the `X-ProxyHost` or `X-Forwarded-Host`
+headers. The host address provided in one of these headers must be configured 
in the `nifi.web.proxy.host` property.
+The application uses the provided alternative host address to construct URLs 
that match the public address of the
+reverse proxy. The server returns an HTTP 421 Misdirected Request status when 
the requested proxy header value is not
+listed in the proxy host address property.
 
 ** NiFi will only accept HTTP requests with a X-ProxyContextPath, 
X-Forwarded-Context, or X-Forwarded-Prefix header if the value is allowed 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
@@ -3520,10 +3526,16 @@ The value can be set to `h2` to require HTTP/2 and 
disable HTTP/1.1.
 |`nifi.web.jetty.working.directory`|The location of the Jetty working 
directory. The default value is `./work/jetty`.
 |`nifi.web.jetty.threads`|The number of Jetty threads. The default value is 
`200`.
 |`nifi.web.max.header.size`|The maximum size allowed for request and response 
headers. The default value is `16 KB`.
-|`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.
-Requests containing an invalid port in the Host or authority header return an 
HTTP 421 Misdirected Request status.
+|`nifi.web.proxy.host`|A comma-separated list of allowed header values to 
consider when the application is running with
+HTTPS enabled and receives requests through a reverse proxy. Each value may 
include a domain name such as
+`nifi.apache.org` or a domain name with a port number such as 
`nifi.apache.org:8443`.
+
+Requests containing an invalid port in the `Host` or authority header return 
an HTTP 421 Misdirected Request status.
+Requests containing an `X-ProxyHost` or `X-Forwarded-Host` header with a value 
not listed in this property return an HTTP
+421 Misdirected Request status.
+
+Reverse proxy servers are responsible for filtering input request headers and 
providing allowed proxy host values to the
+application. Proxy host header values must be limited to including the domain 
name, without the port number.
 |`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.
 |`nifi.web.max.content.size`|The maximum size (HTTP `Content-Length`) for PUT 
and POST requests. No default value is set for backward compatibility. 
Providing a value for this property enables the `Content-Length` filter on all 
incoming API requests (except Site-to-Site and cluster communications). A 
suggested value is `20 MB`.
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java
index 24ce43a51fd..e085cc0face 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/FrameworkServerConnectorFactory.java
@@ -44,6 +44,8 @@ public class FrameworkServerConnectorFactory extends 
StandardServerConnectorFact
 
     private static final String CIPHER_SUITE_SEPARATOR_PATTERN = ",\\s*";
 
+    private static final Pattern HOST_PORT_SEPARATOR = Pattern.compile(":");
+
     private static final Pattern HOST_PORT_PATTERN = 
Pattern.compile(".+?:(\\d+)$");
 
     private static final int PORT_GROUP = 1;
@@ -54,6 +56,8 @@ public class FrameworkServerConnectorFactory extends 
StandardServerConnectorFact
 
     private final String excludeCipherSuites;
 
+    private final Set<String> validProxyHosts;
+
     private final Set<Integer> validPorts;
 
     private SslContextFactory.Server sslContextFactory;
@@ -70,6 +74,7 @@ public class FrameworkServerConnectorFactory extends 
StandardServerConnectorFact
         includeCipherSuites = 
properties.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_INCLUDE);
         excludeCipherSuites = 
properties.getProperty(NiFiProperties.WEB_HTTPS_CIPHERSUITES_EXCLUDE);
         headerSize = DataUnit.parseDataSize(properties.getWebMaxHeaderSize(), 
DataUnit.B).intValue();
+        validProxyHosts = getValidProxyHosts(properties);
         validPorts = getValidPorts(properties);
 
         if (properties.isHTTPSConfigured()) {
@@ -99,9 +104,12 @@ public class FrameworkServerConnectorFactory extends 
StandardServerConnectorFact
         httpConfiguration.setResponseHeaderSize(headerSize);
         httpConfiguration.setIdleTimeout(IDLE_TIMEOUT);
 
-        // Add HostHeaderCustomizer to set Host Header for HTTP/2 and 
HostHeaderHandler
+        // Add HostHeaderCustomizer to set Host Header for HTTP/2
         httpConfiguration.addCustomizer(new HostHeaderCustomizer());
 
+        final ProxyHeaderValidatorCustomizer proxyHeaderValidatorCustomizer = 
new ProxyHeaderValidatorCustomizer(validProxyHosts);
+        httpConfiguration.addCustomizer(proxyHeaderValidatorCustomizer);
+
         final HostPortValidatorCustomizer hostPortValidatorCustomizer = new 
HostPortValidatorCustomizer(validPorts);
         httpConfiguration.addCustomizer(hostPortValidatorCustomizer);
 
@@ -159,6 +167,19 @@ public class FrameworkServerConnectorFactory extends 
StandardServerConnectorFact
         return ObjectUtils.getIfNull(httpsPort, httpPort);
     }
 
+    private static Set<String> getValidProxyHosts(final NiFiProperties 
properties) {
+        final Set<String> validProxyHosts = new HashSet<>();
+
+        final List<String> allowedHosts = properties.getAllowedHostsAsList();
+        for (final String allowedHost : allowedHosts) {
+            final String[] hostPort = HOST_PORT_SEPARATOR.split(allowedHost);
+            final String host = hostPort[0];
+            validProxyHosts.add(host);
+        }
+
+        return validProxyHosts;
+    }
+
     private static Set<Integer> getValidPorts(final NiFiProperties properties) 
{
         final Set<Integer> validPorts = new HashSet<>();
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/ProxyHeaderValidatorCustomizer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/ProxyHeaderValidatorCustomizer.java
new file mode 100644
index 00000000000..4ed71ea7877
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/connector/ProxyHeaderValidatorCustomizer.java
@@ -0,0 +1,91 @@
+/*
+ * 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.server.connector;
+
+import org.apache.nifi.web.servlet.shared.ProxyHeader;
+import org.eclipse.jetty.http.HttpException;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.Request;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Jetty Request Customizer implementing validation for supported Proxy 
Request Headers
+ */
+public class ProxyHeaderValidatorCustomizer implements 
HttpConfiguration.Customizer {
+    private static final String MISDIRECTED_REQUEST_REASON = "Invalid Proxy 
Host Requested";
+
+    private static final Set<String> SUPPORTED_PROXY_HOST_HEADERS = Set.of(
+            ProxyHeader.PROXY_HOST.getHeader(),
+            ProxyHeader.FORWARDED_HOST.getHeader()
+    );
+
+    private final Set<String> validProxyHosts;
+
+    public ProxyHeaderValidatorCustomizer(final Set<String> validProxyHosts) {
+        this.validProxyHosts = Objects.requireNonNull(validProxyHosts, "Valid 
Proxy Hosts required");
+    }
+
+    /**
+     * Validate requested proxy host header values against allowed proxy hosts 
and throw HTTP 421 on invalid values
+     *
+     * @param request HTTP Request to be evaluated
+     * @param responseHeaders HTTP Response headers
+     * @return Valid HTTP Request
+     */
+    @Override
+    public Request customize(final Request request, final HttpFields.Mutable 
responseHeaders) {
+        final Request customized;
+
+        if (request.isSecure()) {
+            customized = customizeSecureRequest(request);
+        } else {
+            customized = request;
+        }
+
+        return customized;
+    }
+
+    private Request customizeSecureRequest(final Request request) {
+        final HttpURI requestUri = request.getHttpURI();
+        final String requestHost = requestUri.getHost();
+
+        final HttpFields requestHeaders = request.getHeaders();
+        for (final String proxyHostHeader : SUPPORTED_PROXY_HOST_HEADERS) {
+            final String hostHeader = requestHeaders.get(proxyHostHeader);
+            // Include empty and blank values for enforced validation of 
request headers
+            if (hostHeader == null) {
+                continue;
+            }
+            // Allow proxy host header matching request host header based on 
TLS SNI and DNS SAN requirements
+            if (requestHost.equals(hostHeader)) {
+                continue;
+            }
+            if (validProxyHosts.contains(hostHeader)) {
+                continue;
+            }
+
+            throw new 
HttpException.RuntimeException(HttpStatus.MISDIRECTED_REQUEST_421, 
MISDIRECTED_REQUEST_REASON);
+        }
+
+        return request;
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/StandardServerProviderTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/StandardServerProviderTest.java
index 8b18bfeb507..825c527b649 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/StandardServerProviderTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/StandardServerProviderTest.java
@@ -22,6 +22,7 @@ import org.apache.nifi.security.ssl.EphemeralKeyStoreBuilder;
 import org.apache.nifi.security.ssl.StandardSslContextBuilder;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.server.handler.HeaderWriterHandler;
+import org.apache.nifi.web.servlet.shared.ProxyHeader;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpStatus;
 import org.eclipse.jetty.rewrite.handler.RewriteHandler;
@@ -83,7 +84,11 @@ class StandardServerProviderTest {
 
     private static final String PUBLIC_HOST = "nifi.apache.org";
 
-    private static final String PUBLIC_UNKNOWN_HOST = "nifi.staged.apache.org";
+    private static final String PUBLIC_STAGED_HOST = "nifi.staged.apache.org";
+
+    private static final String PROXY_HOST_PROPERTY = 
"nifi.apache.org,nifi.staged.apache.org:8443";
+
+    private static final String PUBLIC_UNKNOWN_HOST = 
"nifi.unknown.apache.org";
 
     private static final String ALLOW_RESTRICTED_HEADERS_PROPERTY = 
"jdk.httpclient.allowRestrictedHeaders";
 
@@ -174,7 +179,7 @@ class StandardServerProviderTest {
     void testGetServerHttpsRequestsCompleted() throws Exception {
         final Properties applicationProperties = new Properties();
         applicationProperties.setProperty(NiFiProperties.WEB_HTTPS_PORT, 
RANDOM_PORT);
-        applicationProperties.setProperty(NiFiProperties.WEB_PROXY_HOST, 
PUBLIC_HOST);
+        applicationProperties.setProperty(NiFiProperties.WEB_PROXY_HOST, 
PROXY_HOST_PROPERTY);
         final NiFiProperties properties = 
NiFiProperties.createBasicNiFiProperties((String) null, applicationProperties);
 
         final StandardServerProvider provider = new 
StandardServerProvider(sslContext);
@@ -254,6 +259,24 @@ class StandardServerProviderTest {
                 .header(HOST_HEADER, PUBLIC_HOST)
                 .build();
         assertResponseStatusCode(httpClient, alternativeNameRequest, 
HttpStatus.MOVED_TEMPORARILY_302);
+
+        final HttpRequest proxyHostRequest = 
HttpRequest.newBuilder(localhostUri)
+                .version(HttpClient.Version.HTTP_1_1)
+                .header(ProxyHeader.PROXY_HOST.getHeader(), 
localhostUri.getHost())
+                .build();
+        assertResponseStatusCode(httpClient, proxyHostRequest, 
HttpStatus.MOVED_TEMPORARILY_302);
+
+        final HttpRequest proxyHostPublicHostRequest = 
HttpRequest.newBuilder(localhostUri)
+                .version(HttpClient.Version.HTTP_1_1)
+                .header(ProxyHeader.PROXY_HOST.getHeader(), PUBLIC_HOST)
+                .build();
+        assertResponseStatusCode(httpClient, proxyHostPublicHostRequest, 
HttpStatus.MOVED_TEMPORARILY_302);
+
+        final HttpRequest forwardedHostRequest = 
HttpRequest.newBuilder(localhostUri)
+                .version(HttpClient.Version.HTTP_1_1)
+                .header(ProxyHeader.FORWARDED_HOST.getHeader(), 
PUBLIC_STAGED_HOST)
+                .build();
+        assertResponseStatusCode(httpClient, forwardedHostRequest, 
HttpStatus.MOVED_TEMPORARILY_302);
     }
 
     void assertBadRequestsCompleted(final HttpClient httpClient, final URI 
localhostUri) throws IOException, InterruptedException {
@@ -276,6 +299,18 @@ class StandardServerProviderTest {
                 .header(HOST_HEADER, LOCALHOST_HTTP_PORT)
                 .build();
         assertResponseStatusCode(httpClient, localhostPortRequest, 
HttpStatus.MISDIRECTED_REQUEST_421);
+
+        final HttpRequest publicUnknownProxyHostRequest = 
HttpRequest.newBuilder(localhostUri)
+                .version(HttpClient.Version.HTTP_1_1)
+                .header(ProxyHeader.PROXY_HOST.getHeader(), 
PUBLIC_UNKNOWN_HOST)
+                .build();
+        assertResponseStatusCode(httpClient, publicUnknownProxyHostRequest, 
HttpStatus.MISDIRECTED_REQUEST_421);
+
+        final HttpRequest publicUnknownForwardedHostRequest = 
HttpRequest.newBuilder(localhostUri)
+                .version(HttpClient.Version.HTTP_1_1)
+                .header(ProxyHeader.FORWARDED_HOST.getHeader(), 
PUBLIC_UNKNOWN_HOST)
+                .build();
+        assertResponseStatusCode(httpClient, 
publicUnknownForwardedHostRequest, HttpStatus.MISDIRECTED_REQUEST_421);
     }
 
     void assertStandardResponseHeadersFound(final HttpResponse<Void> response) 
{

Reply via email to