Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
* <p>Solves CVE-2026-XXXX vulnerabilities E (weak DES 56-bit cipher) and F (no integrity /
* anti-tamper protection) when V2 mode is enabled.
*
* <p>V2 (AES-256-GCM): key derivation via PBKDF2WithHmacSHA256 — deterministic across JVM
* vendors. GCM AEAD provides both confidentiality and integrity in one pass.
* <p>V2 (AES-256-GCM): key derivation via PBKDF2WithHmacSHA256 — deterministic across JVM vendors.
* GCM AEAD provides both confidentiality and integrity in one pass.
*
* <p>V1 (DES): retained for backward compatibility. When the V2 switch is toggled off, new tickets
* are issued in the legacy DES format so that downstream systems that have not yet upgraded can
Expand All @@ -54,8 +54,7 @@ public class TicketCipher {
private static final int IV_LEN_BYTES = 12; // GCM standard 96-bit IV
private static final int TAG_LEN_BITS = 128; // GCM auth tag
private static final int PBKDF2_ITERATIONS = 10000;
private static final byte[] PBKDF2_SALT =
"linkis-ticket-v2".getBytes(StandardCharsets.UTF_8);
private static final byte[] PBKDF2_SALT = "linkis-ticket-v2".getBytes(StandardCharsets.UTF_8);
private static final byte VERSION_V2 = 0x02;

private final SecretKey encKey;
Expand Down Expand Up @@ -93,9 +92,9 @@ public String encrypt(String plaintext) throws Exception {
}

/**
* Decrypt data. Auto-detects V2 (version byte 0x02) vs legacy DES.
* V2 tampering is rejected with AEADBadTagException.
* V2 tickets issued before a rollback are still decrypted correctly even when V2 is disabled.
* Decrypt data. Auto-detects V2 (version byte 0x02) vs legacy DES. V2 tampering is rejected with
* AEADBadTagException. V2 tickets issued before a rollback are still decrypted correctly even
* when V2 is disabled.
*/
public String decrypt(String data) throws Exception {
if (StringUtils.isBlank(data)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ public enum LinkisModuleErrorCodeSummary implements LinkisErrorCode {
"The receive queue for WebSocket is full, please try again later(WebSocket的接收队列已满,请稍后重试)!"),
WEBSOCKET_STOPPED(
11035,
"WebSocket consumer has stopped, please contact the administrator to handle(WebSocket的消费器已停止,请联系管理员处理)!");
"WebSocket consumer has stopped, please contact the administrator to handle(WebSocket的消费器已停止,请联系管理员处理)!"),
CRYPT_KEY_INSECURE(
11036,
"The wds.linkis.crypt.key is missing or insecure, refusing to start(wds.linkis.crypt.key 缺失或不安全,拒绝启动)");

/** error code(错误码) */
private final int errorCode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ object ServerConfiguration extends Logging {
private val CRYPT_KEY_MIN_LENGTH = 16

private val cryptKeyRaw: String = CommonVars("wds.linkis.crypt.key", "").getValue
private val allowInsecureCryptKey: Boolean = CommonVars("linkis.crypt.key.allow.insecure", "false").getValue.toBoolean

private val allowInsecureCryptKey: Boolean =
CommonVars("linkis.crypt.key.allow.insecure", "false").getValue.toBoolean

private def validateCryptKey(key: String): Unit = {
val issues = scala.collection.mutable.ArrayBuffer.empty[String]
Expand Down Expand Up @@ -92,8 +94,8 @@ object ServerConfiguration extends Logging {
// have not yet been upgraded. Decryption auto-detects both formats.
private val useTicketCipherV2: Boolean =
CommonVars("linkis.ticket.cipher.v2.enabled", "true").getValue.toBoolean
val ticketCipher = new TicketCipher(cryptKeyRaw, useTicketCipherV2)

val ticketCipher = new TicketCipher(cryptKeyRaw, useTicketCipherV2)

private val ticketHeader = CommonVars("wds.linkis.ticket.header", "bfs_").getValue

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ object SSOUtils extends Logging {
case _: LoginExpireException =>
logger.warn(
"Ignore-timeout path rejected ticket: valid decryption " +
"but ticket not present in server-side session map")
"but ticket not present in server-side session map"
)
None
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

package org.apache.linkis.server.security

import org.apache.linkis.common.conf.Configuration
import org.apache.linkis.common.conf.{CommonVars, Configuration}
import org.apache.linkis.common.utils.{Logging, RSAUtils, Utils}
import org.apache.linkis.errorcode.LinkisModuleErrorCodeSummary.ILLEGAL_USER_TOKEN
import org.apache.linkis.server.{Message, _}
Expand Down Expand Up @@ -196,14 +196,16 @@ object SecurityFilter {
// 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"))
_.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")
"linkis.security.trusted.internal.sources; rejecting as potential auth bypass"
)
return false
}
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@

package org.apache.linkis.server.security

import javax.servlet.http.{Cookie, HttpServletRequest}

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 = {
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)
Expand All @@ -49,59 +52,58 @@ class SecurityFilterTest {
@Test
@DisplayName("isRequestIgnoreTimeout_returnsTrueWhenCookieFromLoopback")
def isRequestIgnoreTimeoutFromLoopbackTest(): Unit = {
val req = mockRequest("127.0.0.1",
cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")))
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")))
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")))
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")))
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")))
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",
val req = mockRequest(
"127.0.0.1",
cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")),
xForwardedFor = "10.0.0.5, 203.0.113.1")
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",
val req = mockRequest(
"203.0.113.1",
cookies = Array(new Cookie(IGNORE_COOKIE_NAME, "true")),
xForwardedFor = "10.0.0.5")
xForwardedFor = "10.0.0.5"
)
Assertions.assertFalse(SecurityFilter.isRequestIgnoreTimeout(req))
}

Expand All @@ -122,4 +124,5 @@ class SecurityFilterTest {
Assertions.assertEquals(-1, cookie.getMaxAge)
Assertions.assertEquals("/", cookie.getPath)
}

}
Loading
Loading