From 45db63f90d5973cd7c623f9c0bd695a0bd0fb0d5 Mon Sep 17 00:00:00 2001 From: aiceflower Date: Thu, 16 Jul 2026 11:49:09 +0800 Subject: [PATCH 1/3] #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 --- .../server/security/SecurityFilter.scala | 51 +++++++++++++++++-- .../gateway/security/GatewaySSOUtils.scala | 13 ++--- 2 files changed, 55 insertions(+), 9 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 b372ead651e..3b8df71b283 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,6 +152,17 @@ 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)) @@ -174,9 +186,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-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 2d696d84105..a8b276822d2 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 } } From de125c84665def4f58d6153a5e914ab03bf6ad54 Mon Sep 17 00:00:00 2001 From: aiceflower Date: Thu, 16 Jul 2026 11:49:38 +0800 Subject: [PATCH 2/3] #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 --- .../linkis/server/security/SecurityFilter.scala | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 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 3b8df71b283..b4f0ae6cb32 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 @@ -166,11 +166,16 @@ object SecurityFilter { 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)) { From 1c96ce7c7927cea2d80494fb67a2f0efa7c484b0 Mon Sep 17 00:00:00 2001 From: aiceflower Date: Thu, 16 Jul 2026 14:23:00 +0800 Subject: [PATCH 3/3] #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 --- .../server/security/SecurityFilterTest.scala | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 linkis-commons/linkis-module/src/test/scala/org/apache/linkis/server/security/SecurityFilterTest.scala 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 00000000000..a68273b21b9 --- /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) + } +}