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]