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)
{