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

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


The following commit(s) were added to refs/heads/master by this push:
     new 9a17b007ec1 fix: reject newlines in redirect locations (#19812)
9a17b007ec1 is described below

commit 9a17b007ec1fd286a8e9bda8fd6aaa6145b4c962
Author: Frank Chen <[email protected]>
AuthorDate: Thu Sep 10 14:06:06 2026 +0800

    fix: reject newlines in redirect locations (#19812)
    
    * Reject newlines in redirect locations
    
    * fix: make redirect sanitization explicit
    
    * fix: satisfy strict test compilation
    
    * test: strengthen redirect validation coverage
    
    * fix: preserve redirect sanitization
    
    * test: migrate redirect filter test to JUnit 5
---
 .../apache/druid/server/http/RedirectFilter.java   |  16 ++-
 .../druid/server/http/RedirectFilterTest.java      | 133 +++++++++++++++++++++
 2 files changed, 145 insertions(+), 4 deletions(-)

diff --git 
a/server/src/main/java/org/apache/druid/server/http/RedirectFilter.java 
b/server/src/main/java/org/apache/druid/server/http/RedirectFilter.java
index d6da48d3686..b0184be1f8d 100644
--- a/server/src/main/java/org/apache/druid/server/http/RedirectFilter.java
+++ b/server/src/main/java/org/apache/druid/server/http/RedirectFilter.java
@@ -72,17 +72,25 @@ public class RedirectFilter implements Filter
     if (redirectInfo.doLocal(request.getRequestURI())) {
       chain.doFilter(request, response);
     } else {
-      URL url = redirectInfo.getRedirectURL(request.getQueryString(), 
request.getRequestURI());
-      log.debug("Forwarding request to [%s]", url);
-
+      final URL url = redirectInfo.getRedirectURL(request.getQueryString(), 
request.getRequestURI());
       if (url == null) {
         // We apparently have nothing to redirect to, so let's do a Service 
Unavailable
         response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
         return;
       }
 
+      final String location = url.toString();
+      if (location.indexOf('\r') >= 0 || location.indexOf('\n') >= 0) {
+        response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+        return;
+      }
+
+      // String.replace returns the same instance when the target is absent, 
so these calls make the validated
+      // location recognizable to security analysis without allocating another 
String.
+      final String validatedLocation = location.replace('\r', ' 
').replace('\n', ' ');
+      log.debug("Forwarding request to [%s]", url);
       response.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
-      response.setHeader("Location", url.toString());
+      response.setHeader("Location", validatedLocation);
     }
   }
 
diff --git 
a/server/src/test/java/org/apache/druid/server/http/RedirectFilterTest.java 
b/server/src/test/java/org/apache/druid/server/http/RedirectFilterTest.java
new file mode 100644
index 00000000000..3c1e62f759a
--- /dev/null
+++ b/server/src/test/java/org/apache/druid/server/http/RedirectFilterTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.druid.server.http;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+@ExtendWith(MockitoExtension.class)
+public class RedirectFilterTest
+{
+  private static final String REQUEST_URI = "/druid/coordinator/v1/loadstatus";
+  private static final String QUERY_STRING = "simple=true";
+
+  @Mock
+  private RedirectInfo redirectInfo;
+  @Mock
+  private HttpServletRequest request;
+  @Mock
+  private HttpServletResponse response;
+  @Mock
+  private FilterChain filterChain;
+
+  private RedirectFilter redirectFilter;
+
+  @BeforeEach
+  public void setUp()
+  {
+    redirectFilter = new RedirectFilter(redirectInfo);
+    Mockito.when(request.getRequestURI()).thenReturn(REQUEST_URI);
+    Mockito.when(request.getQueryString()).thenReturn(QUERY_STRING);
+    Mockito.when(redirectInfo.doLocal(REQUEST_URI)).thenReturn(false);
+  }
+
+  @Test
+  public void testRedirect() throws Exception
+  {
+    final String location = "https://leader.example:8081"; + REQUEST_URI + "?" 
+ QUERY_STRING + "#result";
+    Mockito.when(redirectInfo.getRedirectURL(QUERY_STRING, 
REQUEST_URI)).thenReturn(URI.create(location).toURL());
+
+    redirectFilter.doFilter(request, response, filterChain);
+
+    
Mockito.verify(response).setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
+    Mockito.verify(response).setHeader("Location", location);
+    Mockito.verifyNoInteractions(filterChain);
+  }
+
+  @Test
+  public void testRedirectPreservesEncodedNewlines() throws Exception
+  {
+    final String location = "https://leader.example/path%0D%0Avalue?query=%0a";;
+    Mockito.when(redirectInfo.getRedirectURL(QUERY_STRING, 
REQUEST_URI)).thenReturn(URI.create(location).toURL());
+
+    redirectFilter.doFilter(request, response, filterChain);
+
+    
Mockito.verify(response).setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
+    Mockito.verify(response).setHeader("Location", location);
+    Mockito.verifyNoInteractions(filterChain);
+  }
+
+  @Test
+  public void testRedirectRejectsCarriageReturn() throws Exception
+  {
+    assertInvalidRedirect("https://leader.example/path\rInjected: value");
+  }
+
+  @Test
+  public void testRedirectRejectsLineFeed() throws Exception
+  {
+    assertInvalidRedirect("https://leader.example/path\nInjected: value");
+  }
+
+  private void assertInvalidRedirect(String location) throws Exception
+  {
+    Mockito.when(redirectInfo.getRedirectURL(QUERY_STRING, 
REQUEST_URI)).thenReturn(urlWithExternalForm(location));
+
+    redirectFilter.doFilter(request, response, filterChain);
+
+    Mockito.verify(response).sendError(HttpServletResponse.SC_BAD_REQUEST);
+    Mockito.verify(response, Mockito.never()).setStatus(Mockito.anyInt());
+    Mockito.verify(response, Mockito.never()).setHeader(Mockito.anyString(), 
Mockito.anyString());
+    Mockito.verifyNoInteractions(filterChain);
+  }
+
+  private static URL urlWithExternalForm(final String externalForm) throws 
Exception
+  {
+    return URL.of(
+        URI.create("https://leader.example";),
+        new URLStreamHandler()
+        {
+          @Override
+          protected URLConnection openConnection(final URL url)
+          {
+            throw new UnsupportedOperationException();
+          }
+
+          @Override
+          protected String toExternalForm(final URL url)
+          {
+            return externalForm;
+          }
+        }
+    );
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to