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

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


The following commit(s) were added to refs/heads/master by this push:
     new 047ebde764 [SECURITY] Add source IP validation for ignore-timeout 
fallback in session ticket auth (#5457)
047ebde764 is described below

commit 047ebde764ff355d1b762bc3394d34ba6a29d13f
Author: aiceflower <[email protected]>
AuthorDate: Wed Aug 12 17:08:34 2026 +0800

    [SECURITY] Add source IP validation for ignore-timeout fallback in session 
ticket auth (#5457)
    
    * #AI COMMIT# [SECURITY] Add source IP validation for ignore-timeout 
cookie, remove cookie fallback from gateway
    
    - Add linkis.security.trusted.internal.sources config (default 
loopback+RFC1918 CIDRs)
    - Add linkis.security.ignore.timeout.require.internal.ip config (default 
true, escape hatch)
    - isRequestIgnoreTimeout: validate client IP against trusted sources before 
honouring cookie
    - Add getClientIp/isTrustedInternal helpers with X-Forwarded-For and 
SubnetUtils CIDR support
    - GatewaySSOUtils.getLoginUser: remove client-cookie-based fallback; 
gateway is external boundary
    
    * #AI COMMIT# [SECURITY] Gate header-based internal RPC fallback behind 
source IP validation
    
    - getLoginUserThrowsExceptionWhenTimeout: require trusted internal IP for
      the header-based OTHER_SYSTEM_IGNORE_UM_USER fallback path
    - Prevents header-forged internal RPC identity from external IPs
    
    * #AI COMMIT# Add unit tests for IP-validated ignore-timeout fallback
    
    - 10 tests covering isRequestIgnoreTimeout with loopback / external / 
RFC1918 IPs
    - X-Forwarded-For honouring from trusted upstream, rejection from untrusted
    - Header-based OTHER_SYSTEM_IGNORE_UM_USER fallback gated behind IP check
    - Cookie value validation and ignoreTimeoutSignal defaults
---
 .../linkis/server/security/SecurityFilter.scala    |  66 +++++++++--
 .../server/security/SecurityFilterTest.scala       | 125 +++++++++++++++++++++
 .../linkis/gateway/security/GatewaySSOUtils.scala  |  13 ++-
 3 files changed, 190 insertions(+), 14 deletions(-)

diff --git 
a/linkis-commons/linkis-module/src/main/scala/org/apache/linkis/server/security/SecurityFilter.scala
 
b/linkis-commons/linkis-module/src/main/scala/org/apache/linkis/server/security/SecurityFilter.scala
index b372ead651..b4f0ae6cb3 100644
--- 
a/linkis-commons/linkis-module/src/main/scala/org/apache/linkis/server/security/SecurityFilter.scala
+++ 
b/linkis-commons/linkis-module/src/main/scala/org/apache/linkis/server/security/SecurityFilter.scala
@@ -31,6 +31,7 @@ import org.apache.linkis.server.security.SecurityFilter.logger
 import org.apache.linkis.server.security.SSOUtils.sslEnable
 
 import org.apache.commons.lang3.StringUtils
+import org.apache.commons.net.util.SubnetUtils
 
 import javax.servlet._
 import javax.servlet.http.{Cookie, HttpServletRequest, HttpServletResponse}
@@ -151,14 +152,30 @@ object SecurityFilter {
   private[linkis] val OTHER_SYSTEM_IGNORE_UM_USER = "dataworkcloud_rpc_user"
   private[linkis] val ALLOW_ACCESS_WITHOUT_TIMEOUT = 
"dataworkcloud_inner_request"
 
+  // CVE-2026-XXXX: trusted internal source CIDRs for gateway-to-backend 
forwarding.
+  // Default: loopback + RFC1918. Override per-deployment to add 
gateway/load-balancer IPs.
+  private val trustedInternalSources: Array[String] =
+    CommonVars(
+      "linkis.security.trusted.internal.sources",
+      "127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
+    ).getValue.split(",").map(_.trim).filter(_.nonEmpty)
+
+  private val requireInternalIp: Boolean =
+    CommonVars("linkis.security.ignore.timeout.require.internal.ip", 
"true").getValue.toBoolean
+
   def getLoginUserThrowsExceptionWhenTimeout(req: HttpServletRequest): 
Option[String] =
     Option(req.getCookies)
       .flatMap(cs => SSOUtils.getLoginUser(cs))
-      .orElse(
-        SSOUtils
-          .getLoginUserIgnoreTimeout(key => Option(req.getHeader(key)))
-          .filter(_ == OTHER_SYSTEM_IGNORE_UM_USER)
-      )
+      .orElse {
+        // Header-based internal RPC fallback must only be honoured for
+        // requests from trusted internal IPs. Reject external forgeries.
+        val clientIp = getClientIp(req)
+        if (!isTrustedInternal(clientIp)) None
+        else
+          SSOUtils
+            .getLoginUserIgnoreTimeout(key => Option(req.getHeader(key)))
+            .filter(_ == OTHER_SYSTEM_IGNORE_UM_USER)
+      }
 
   def getLoginUser(req: HttpServletRequest): Option[String] =
     Utils.tryCatch(getLoginUserThrowsExceptionWhenTimeout(req)) {
@@ -174,9 +191,42 @@ object SecurityFilter {
       case t => throw t
     }
 
-  def isRequestIgnoreTimeout(req: HttpServletRequest): Boolean = 
Option(req.getCookies).exists(
-    _.exists(c => c.getName == ALLOW_ACCESS_WITHOUT_TIMEOUT && c.getValue == 
"true")
-  )
+  // CVE-2026-XXXX (vuln B): the ignore-timeout cookie must only be honoured 
when
+  // the request originates from a trusted internal source (gateway or 
same-network
+  // peer). External clients cannot set this cookie to trigger the bypass.
+  def isRequestIgnoreTimeout(req: HttpServletRequest): Boolean = {
+    val hasCookie = Option(req.getCookies).exists(
+      _.exists(c => c.getName == ALLOW_ACCESS_WITHOUT_TIMEOUT && c.getValue == 
"true"))
+    if (!hasCookie) return false
+    if (!requireInternalIp) return true // escape hatch for legacy deployments
+    val ip = getClientIp(req)
+    if (!isTrustedInternal(ip)) {
+      logger.warn(
+        s"Ignore-timeout cookie present but client IP $ip is not in " +
+          "linkis.security.trusted.internal.sources; rejecting as potential 
auth bypass")
+      return false
+    }
+    true
+  }
+
+  private def getClientIp(req: HttpServletRequest): String = {
+    val remote = req.getRemoteAddr
+    // Honour X-Forwarded-For only when the immediate upstream is already 
trusted
+    if (isTrustedInternal(remote)) {
+      val xff = req.getHeader("X-Forwarded-For")
+      if (xff != null && xff.nonEmpty) return xff.split(",").head.trim
+    }
+    remote
+  }
+
+  private def isTrustedInternal(ip: String): Boolean = {
+    if (ip == null || ip.isEmpty) return false
+    if (trustedInternalSources.contains(ip)) return true
+    trustedInternalSources.exists { cidr =>
+      try { new SubnetUtils(cidr).getInfo.isInRange(ip) }
+      catch { case _: Exception => false }
+    }
+  }
 
   def addIgnoreTimeoutSignal(response: HttpServletResponse): Unit =
     response.addCookie(ignoreTimeoutSignal())
diff --git 
a/linkis-commons/linkis-module/src/test/scala/org/apache/linkis/server/security/SecurityFilterTest.scala
 
b/linkis-commons/linkis-module/src/test/scala/org/apache/linkis/server/security/SecurityFilterTest.scala
new file mode 100644
index 0000000000..a68273b21b
--- /dev/null
+++ 
b/linkis-commons/linkis-module/src/test/scala/org/apache/linkis/server/security/SecurityFilterTest.scala
@@ -0,0 +1,125 @@
+/*
+ * 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.linkis.server.security
+
+import org.junit.jupiter.api.{Assertions, DisplayName, Test}
+import org.mockito.Mockito.{mock, when}
+
+import javax.servlet.http.{Cookie, HttpServletRequest}
+
+class SecurityFilterTest {
+
+  private val IGNORE_COOKIE_NAME = SecurityFilter.ALLOW_ACCESS_WITHOUT_TIMEOUT
+
+  private def mockRequest(remoteAddr: String, cookies: Array[Cookie] = null,
+                          xForwardedFor: String = null): HttpServletRequest = {
+    val req = mock(classOf[HttpServletRequest])
+    when(req.getRemoteAddr).thenReturn(remoteAddr)
+    when(req.getCookies).thenReturn(cookies)
+    if (xForwardedFor != null) {
+      when(req.getHeader("X-Forwarded-For")).thenReturn(xForwardedFor)
+    } else {
+      when(req.getHeader("X-Forwarded-For")).thenReturn(null)
+    }
+    req
+  }
+
+  @Test
+  @DisplayName("isRequestIgnoreTimeout_returnsFalseWhenCookieAbsent")
+  def isRequestIgnoreTimeoutNoCookieTest(): Unit = {
+    val req = mockRequest("127.0.0.1", cookies = null)
+    Assertions.assertFalse(SecurityFilter.isRequestIgnoreTimeout(req))
+  }
+
+  @Test
+  @DisplayName("isRequestIgnoreTimeout_returnsTrueWhenCookieFromLoopback")
+  def isRequestIgnoreTimeoutFromLoopbackTest(): Unit = {
+    val req = mockRequest("127.0.0.1",
+      cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")))
+    Assertions.assertTrue(SecurityFilter.isRequestIgnoreTimeout(req))
+  }
+
+  @Test
+  @DisplayName("isRequestIgnoreTimeout_returnsFalseWhenCookieFromExternalIP")
+  def isRequestIgnoreTimeoutFromExternalIPTest(): Unit = {
+    // 203.0.113.0/24 is TEST-NET-3, not in default RFC1918 trusted sources
+    val req = mockRequest("203.0.113.1",
+      cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")))
+    Assertions.assertFalse(SecurityFilter.isRequestIgnoreTimeout(req))
+  }
+
+  @Test
+  @DisplayName("isRequestIgnoreTimeout_returnsTrueFromPrivate10x")
+  def isRequestIgnoreTimeoutFromPrivate10xTest(): Unit = {
+    val req = mockRequest("10.255.255.1",
+      cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")))
+    Assertions.assertTrue(SecurityFilter.isRequestIgnoreTimeout(req))
+  }
+
+  @Test
+  @DisplayName("isRequestIgnoreTimeout_returnsTrueFromPrivate172x")
+  def isRequestIgnoreTimeoutFromPrivate172xTest(): Unit = {
+    val req = mockRequest("172.31.0.1",
+      cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")))
+    Assertions.assertTrue(SecurityFilter.isRequestIgnoreTimeout(req))
+  }
+
+  @Test
+  @DisplayName("isRequestIgnoreTimeout_returnsFalseWhenCookieValueIsFalse")
+  def isRequestIgnoreTimeoutCookieFalseTest(): Unit = {
+    val req = mockRequest("127.0.0.1",
+      cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "false")))
+    Assertions.assertFalse(SecurityFilter.isRequestIgnoreTimeout(req))
+  }
+
+  @Test
+  @DisplayName("isRequestIgnoreTimeout_honorsXForwardedForFromTrustedUpstream")
+  def isRequestIgnoreTimeoutWithXForwardedForTest(): Unit = {
+    val req = mockRequest("127.0.0.1",
+      cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")),
+      xForwardedFor = "10.0.0.5, 203.0.113.1")
+    Assertions.assertTrue(SecurityFilter.isRequestIgnoreTimeout(req))
+  }
+
+  @Test
+  
@DisplayName("isRequestIgnoreTimeout_ignoresXForwardedForFromUntrustedUpstream")
+  def isRequestIgnoreTimeoutXForwardedForUntrustedTest(): Unit = {
+    val req = mockRequest("203.0.113.1",
+      cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")),
+      xForwardedFor = "10.0.0.5")
+    Assertions.assertFalse(SecurityFilter.isRequestIgnoreTimeout(req))
+  }
+
+  @Test
+  
@DisplayName("getLoginUserThrowsExceptionWhenTimeout_rejectsHeaderFallbackFromExternalIP")
+  def getLoginUserHeaderFallbackFromExternalIPTest(): Unit = {
+    val req = mockRequest("203.0.113.1", cookies = null)
+    val result = SecurityFilter.getLoginUserThrowsExceptionWhenTimeout(req)
+    Assertions.assertTrue(result.isEmpty)
+  }
+
+  @Test
+  @DisplayName("ignoreTimeoutSignal_createsCookieWithCorrectDefaults")
+  def ignoreTimeoutSignalDefaultsTest(): Unit = {
+    val cookie = SecurityFilter.ignoreTimeoutSignal()
+    Assertions.assertEquals(IGNORE_COOKIE_NAME, cookie.getName)
+    Assertions.assertEquals("true", cookie.getValue)
+    Assertions.assertEquals(-1, cookie.getMaxAge)
+    Assertions.assertEquals("/", cookie.getPath)
+  }
+}
diff --git 
a/linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-core/src/main/scala/org/apache/linkis/gateway/security/GatewaySSOUtils.scala
 
b/linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-core/src/main/scala/org/apache/linkis/gateway/security/GatewaySSOUtils.scala
index 2d696d8410..a8b276822d 100644
--- 
a/linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-core/src/main/scala/org/apache/linkis/gateway/security/GatewaySSOUtils.scala
+++ 
b/linkis-spring-cloud-services/linkis-service-gateway/linkis-gateway-core/src/main/scala/org/apache/linkis/gateway/security/GatewaySSOUtils.scala
@@ -65,18 +65,19 @@ object GatewaySSOUtils extends Logging {
     case _ => host
   }
 
+  // CVE-2026-XXXX (vuln B): the gateway is the external trust boundary and
+  // must never honour the client-controllable dataworkcloud_inner_request
+  // cookie. Only the header-based OTHER_SYSTEM_IGNORE_UM_USER internal RPC
+  // path remains as a fallback.
   def getLoginUser(gatewayContext: GatewayContext): Option[String] = {
     val cookies = getCookies(gatewayContext)
     Utils.tryCatch(SSOUtils.getLoginUser(cookies)) {
-      case _: LoginExpireException
-          if Option(cookies).exists(
-            _.exists(c => c.getName == ALLOW_ACCESS_WITHOUT_TIMEOUT && 
c.getValue == "true")
-          ) =>
+      case _: LoginExpireException =>
         ServerSSOUtils
           .getLoginUserIgnoreTimeout(key =>
-            Option(cookies).flatMap(_.find(_.getName == key).map(_.getValue))
+            
Option(gatewayContext.getRequest.getHeaders.get(key)).flatMap(_.headOption)
           )
-          .filter(_ != OTHER_SYSTEM_IGNORE_UM_USER)
+          .filter(_ == OTHER_SYSTEM_IGNORE_UM_USER)
       case t => throw t
     }
   }


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

Reply via email to