From d6fb98bb1376357991bbf26fadba5597a722d18b Mon Sep 17 00:00:00 2001 From: lettimepassby <86416409@qq.com> Date: Wed, 24 Jun 2026 14:35:31 +0800 Subject: [PATCH 01/21] fix: add missing comma after date column in CSV export --- .../chen/framework/console/dataview/export/DataExport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java index 8b3a10b0..4c7c1d74 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/console/dataview/export/DataExport.java @@ -141,6 +141,7 @@ public void exportData(String path, DataViewData data) throws Exception { } else if (obj instanceof Date) { SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); writeString(writer, fmt.format(obj)); + writer.write(","); } else { writeString(writer, row.get(field.getName())); writer.write(","); From 4c1c73fe48865cf3f5ee4f5588e9bd38b7c7a21b Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Mon, 29 Jun 2026 13:28:59 +0800 Subject: [PATCH 02/21] fix(mysql): allow public key retrieval for JDBC connections --- .../mysql/MysqlConnectionManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlConnectionManager.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlConnectionManager.java index 8d08025d..cc83f291 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlConnectionManager.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlConnectionManager.java @@ -17,7 +17,7 @@ public class MysqlConnectionManager extends BaseConnectionManager { - private static final String jdbcUrlTemplate = "jdbc:mysql://${host}:${port}/${db}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=CONVERT_TO_NULL&tinyInt1isBit=false&jdbcCompliantTruncation=false"; + private static final String jdbcUrlTemplate = "jdbc:mysql://${host}:${port}/${db}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=CONVERT_TO_NULL&tinyInt1isBit=false&jdbcCompliantTruncation=false&allowPublicKeyRetrieval=true"; private String jdbcUrl; public MysqlConnectionManager(DBConnectInfo connectInfo, Datasource datasource) { From ed13f08f0077b47011c9bef2806a3ff129cf1cbe Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Mon, 13 Jul 2026 14:28:28 +0800 Subject: [PATCH 03/21] fix: prevent unaudited resource action SQL injection --- .../datasource/base/BaseActionHandler.java | 11 ++++++- .../chen/web/service/ResourceService.java | 29 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseActionHandler.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseActionHandler.java index a6136410..7305c3a8 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseActionHandler.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseActionHandler.java @@ -13,6 +13,7 @@ import org.jumpserver.chen.framework.session.SessionManager; import org.jumpserver.chen.framework.utils.LangUtils; import org.jumpserver.chen.framework.utils.TreeUtils; +import org.jumpserver.wisp.Common; import java.lang.reflect.InvocationTargetException; import java.sql.SQLException; @@ -162,7 +163,15 @@ public EventEmitter onShowProperties(TreeNode node) { public EventEmitter onShowObjectProperties(String type, String sql, TreeNode node) throws SQLException { var sqlActuator = this.getDatasource().getConnectionManager().getSqlActuator(); var objName = TreeUtils.getValue(node.getKey(), type); - var result = sqlActuator.execute(SQL.of(sql, objName)); + var command = SQL.of(sql, objName.replace("'", "''")); + var aclResult = SessionManager.getCurrentSession().checkACL(command.getSql()); + if (aclResult != null && (aclResult.getRiskLevel() == Common.RiskLevel.Reject + || aclResult.getRiskLevel() == Common.RiskLevel.ReviewReject)) { + throw new SQLException("Resource action rejected by ACL"); + } + var plan = sqlActuator.createPlan(command); + plan.setAclResult(aclResult); + var result = sqlActuator.executeWithAudit(plan); var detailDialog = new DetailDialog(node.getKey(), type + MessageUtils.get("Properties")); detailDialog.setWidth("50%"); diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/service/ResourceService.java b/backend/web/src/main/java/org/jumpserver/chen/web/service/ResourceService.java index 79f2fbce..74a82a6b 100644 --- a/backend/web/src/main/java/org/jumpserver/chen/web/service/ResourceService.java +++ b/backend/web/src/main/java/org/jumpserver/chen/web/service/ResourceService.java @@ -5,11 +5,13 @@ import org.jumpserver.chen.framework.datasource.entity.action.Action; import org.jumpserver.chen.framework.datasource.entity.form.FormData; import org.jumpserver.chen.framework.session.SessionManager; +import org.jumpserver.chen.framework.utils.TreeUtils; import org.jumpserver.chen.web.exception.ChenException; import org.springframework.stereotype.Service; import java.sql.SQLException; import java.util.List; +import java.util.Objects; @Service @@ -32,12 +34,37 @@ public List getActions(TreeNode node) { public EventEmitter doAction(TreeNode node, String action) { try { var ds = SessionManager.getCurrentSession().getDatasource(); - return ds.doAction(node, action); + var resolvedNode = this.resolveActionNode(ds, node, action); + return ds.doAction(resolvedNode, action); } catch (Exception e) { + if (e instanceof ChenException) { + throw (ChenException) e; + } throw new ChenException(String.format("执行节点动作 %s 失败", node.getLabel()), e); } } + private TreeNode resolveActionNode(org.jumpserver.chen.framework.datasource.Datasource datasource, + TreeNode requestedNode, String action) throws SQLException { + if (requestedNode == null || requestedNode.getKey() == null || requestedNode.getKey().isBlank() + || action == null || action.isBlank()) { + throw new ChenException("Invalid resource action"); + } + + var root = datasource.getResourceBrowser().getTree(); + var resolvedNode = root == null ? null : TreeUtils.getNode(root, requestedNode.getKey()); + if (resolvedNode == null || !Objects.equals(resolvedNode.getType(), requestedNode.getType())) { + throw new ChenException("Invalid resource node"); + } + + var exposed = datasource.getActions(resolvedNode).stream() + .anyMatch(candidate -> Objects.equals(candidate.getKey(), action)); + if (!exposed) { + throw new ChenException("Invalid resource action"); + } + return resolvedNode; + } + public EventEmitter submitResourceForm(FormData form) throws SQLException { var ds = SessionManager.getCurrentSession().getDatasource(); return ds.handleForm(form); From c25629ff2615946d384bcc22c41f51275771b50f Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Mon, 13 Jul 2026 14:41:48 +0800 Subject: [PATCH 04/21] fix: avoid auditing resource property queries as commands --- .../framework/datasource/base/BaseActionHandler.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseActionHandler.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseActionHandler.java index 7305c3a8..f2fa7bd7 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseActionHandler.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseActionHandler.java @@ -13,7 +13,6 @@ import org.jumpserver.chen.framework.session.SessionManager; import org.jumpserver.chen.framework.utils.LangUtils; import org.jumpserver.chen.framework.utils.TreeUtils; -import org.jumpserver.wisp.Common; import java.lang.reflect.InvocationTargetException; import java.sql.SQLException; @@ -164,14 +163,8 @@ public EventEmitter onShowObjectProperties(String type, String sql, TreeNode nod var sqlActuator = this.getDatasource().getConnectionManager().getSqlActuator(); var objName = TreeUtils.getValue(node.getKey(), type); var command = SQL.of(sql, objName.replace("'", "''")); - var aclResult = SessionManager.getCurrentSession().checkACL(command.getSql()); - if (aclResult != null && (aclResult.getRiskLevel() == Common.RiskLevel.Reject - || aclResult.getRiskLevel() == Common.RiskLevel.ReviewReject)) { - throw new SQLException("Resource action rejected by ACL"); - } - var plan = sqlActuator.createPlan(command); - plan.setAclResult(aclResult); - var result = sqlActuator.executeWithAudit(plan); + log.info("resource action show_properties: type={}, node={}", type, node.getKey()); + var result = sqlActuator.execute(command); var detailDialog = new DetailDialog(node.getKey(), type + MessageUtils.get("Properties")); detailDialog.setWidth("50%"); From 417c59f07a34c75a3bc3427996aa2523c77f35c8 Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Tue, 14 Jul 2026 15:49:16 +0800 Subject: [PATCH 05/21] fix: harden WebSocket session binding and export paths --- .../framework/session/SessionManager.java | 16 +++++ .../framework/ws/SessionWebSocketHandler.java | 14 ++++- .../chen/web/config/WebSocketConfig.java | 58 ++++++++++++++----- .../chen/web/controller/AuthController.java | 2 + .../web/controller/ConsoleController.java | 15 ++++- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/session/SessionManager.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/session/SessionManager.java index 6778a070..659ad02b 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/session/SessionManager.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/session/SessionManager.java @@ -9,9 +9,13 @@ @Slf4j public class SessionManager { + // 绑定创建 Chen 会话的 Servlet HTTP session,WS 握手时用它阻止 token 被跨浏览器重放。 + public static final String WEB_SESSION_ID_ATTRIBUTE = "webSessionId"; private final static SessionManager instance = new SessionManager(); private final static ThreadLocal token = new ThreadLocal<>(); private final Map store = new ConcurrentHashMap<>(); + // 每个 Chen 会话只能有一个主 /ws/session 连接,值为 WebSocket session id。 + private final Map primaryWebSockets = new ConcurrentHashMap<>(); public static String registerSession(Session session) { String token = createToken(); @@ -23,6 +27,7 @@ public static String registerSession(Session session) { public static void unregisterSession(String token) { instance.store.remove(token); + instance.primaryWebSockets.remove(token); log.info("session {} unregistered, current session count {}", token, instance.getCurrentSessionCount()); } @@ -54,6 +59,17 @@ public static Session getSession(String token) { return instance.store.get(token); } + public static boolean claimPrimaryWebSocket(String token, String webSocketId) { + // 原子占用,避免并发握手同时替换当前主连接。 + String existing = instance.primaryWebSockets.putIfAbsent(token, webSocketId); + return existing == null || existing.equals(webSocketId); + } + + public static boolean releasePrimaryWebSocket(String token, String webSocketId) { + // 只允许占用者释放,防止被拒绝的重放连接关闭正常会话。 + return instance.primaryWebSockets.remove(token, webSocketId); + } + private static String createToken() { return UUID.randomUUID().toString().replace("-", ""); diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/ws/SessionWebSocketHandler.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/ws/SessionWebSocketHandler.java index 1c0b2a0f..3b56263b 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/ws/SessionWebSocketHandler.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/ws/SessionWebSocketHandler.java @@ -30,6 +30,13 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio } var token = (String) session.getAttributes().get("token"); + // 拒绝第二个主会话,避免重放连接替换 PacketIO 并在关闭时终止原会话。 + if (!SessionManager.claimPrimaryWebSocket(token, session.getId())) { + log.warn("Reject duplicate primary WebSocket connection"); + session.close(CloseStatus.POLICY_VIOLATION); + return; + } + log.info("Primary WebSocket connection established"); SessionManager.setContext(token); Session sess = SessionManager.getCurrentSession(); @@ -96,10 +103,15 @@ public void handleMessage(WebSocketSession session, WebSocketMessage message) @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { var token = (String) session.getAttributes().get("token"); + // 被拒绝的连接没有占用主会话,不得影响当前仍在线的用户。 + if (!SessionManager.releasePrimaryWebSocket(token, session.getId())) { + return; + } + log.info("Primary WebSocket connection closed: code={}", closeStatus.getCode()); SessionManager.setContext(token); var sess = SessionManager.getCurrentSession(); if (sess != null) { sess.close(); } } -} \ No newline at end of file +} diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/config/WebSocketConfig.java b/backend/web/src/main/java/org/jumpserver/chen/web/config/WebSocketConfig.java index 690145c2..3e79d5c1 100644 --- a/backend/web/src/main/java/org/jumpserver/chen/web/config/WebSocketConfig.java +++ b/backend/web/src/main/java/org/jumpserver/chen/web/config/WebSocketConfig.java @@ -1,6 +1,7 @@ package org.jumpserver.chen.web.config; import lombok.extern.slf4j.Slf4j; +import org.jumpserver.chen.framework.session.SessionManager; import org.jumpserver.chen.framework.ws.ConsoleWebSocketHandler; import org.jumpserver.chen.framework.ws.DBConsoleWebsocketHandler; import org.jumpserver.chen.framework.ws.SessionWebSocketHandler; @@ -9,12 +10,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.config.annotation.EnableWebSocket; -import org.springframework.web.socket.config.annotation.WebSocketConfigurer; -import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.server.HandshakeInterceptor; import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; +import org.springframework.web.socket.server.support.WebSocketHandlerMapping; +import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler; import java.net.InetSocketAddress; import java.net.URI; @@ -22,9 +23,8 @@ import java.util.stream.Collectors; @Configuration -@EnableWebSocket @Slf4j -public class WebSocketConfig implements WebSocketConfigurer { +public class WebSocketConfig { @Bean @@ -38,14 +38,25 @@ public ServletServerContainerFactoryBean createWebSocketContainer() { } - @Override - public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry - .addHandler(new ConsoleWebSocketHandler(), "/ws/console") - .addHandler(new SessionWebSocketHandler(), "/ws/session") - .addHandler(new DBConsoleWebsocketHandler(), "/ws/db-console") - .addInterceptors(new ServletWebSocketHandshakeInterceptor()) - .setAllowedOrigins("*"); + @Bean + public WebSocketHandlerMapping chenWebSocketHandlerMapping() { + var handlers = new LinkedHashMap(); + handlers.put("/ws/session", createRequestHandler(new SessionWebSocketHandler())); + handlers.put("/ws/console", createRequestHandler(new ConsoleWebSocketHandler())); + handlers.put("/ws/db-console", createRequestHandler(new DBConsoleWebsocketHandler())); + + var mapping = new WebSocketHandlerMapping(); + // 与 Spring 默认 WebSocket 映射一致,确保 WS 请求优先于普通 MVC 映射处理。 + mapping.setOrder(1); + mapping.setUrlMap(handlers); + return mapping; + } + + private WebSocketHttpRequestHandler createRequestHandler(WebSocketHandler webSocketHandler) { + var requestHandler = new WebSocketHttpRequestHandler(webSocketHandler); + // 仅使用 Chen 的动态校验,避免 WebSocketHandlerRegistry 追加第二个 Origin 拦截器。 + requestHandler.setHandshakeInterceptors(List.of(new ServletWebSocketHandshakeInterceptor())); + return requestHandler; } // 仅用于跨 Host 请求的精确白名单,格式为逗号分隔的 host 或 host:port。 @@ -146,6 +157,12 @@ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse res String origin = request.getHeaders().getOrigin(); InetSocketAddress requestHost = request.getHeaders().getHost(); + // 某些 Servlet 请求对象未填充 Host header,回退到容器解析到的主机和端口。 + if (requestHost == null && request instanceof ServletServerHttpRequest servletRequest) { + requestHost = new InetSocketAddress( + servletRequest.getServletRequest().getServerName(), + servletRequest.getServletRequest().getServerPort()); + } if (!checkOrigin(origin, requestHost, TRUSTED_DOMAINS)) { log.warn("Reject WebSocket handshake: untrusted or invalid origin"); @@ -161,6 +178,21 @@ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse res } var token = protocols.get(0); + var session = SessionManager.getSession(token); + var servletRequest = request instanceof ServletServerHttpRequest servletRequestWrapper + ? servletRequestWrapper.getServletRequest() : null; + var httpSession = servletRequest == null ? null : servletRequest.getSession(false); + var sessionBound = session != null && httpSession != null && Objects.equals( + session.getAttribute(SessionManager.WEB_SESSION_ID_ATTRIBUTE), httpSession.getId()); + // token 必须仍存活,且必须来自创建它的同一浏览器 HTTP session。 + if (!sessionBound) { + // 仅输出校验维度,不记录 token、Cookie 或 HTTP session ID 等敏感信息。 + log.warn("Reject WebSocket handshake: tokenExists={}, httpSessionExists={}, sessionBound={}", + session != null, httpSession != null, sessionBound); + response.setStatusCode(HttpStatus.UNAUTHORIZED); + return false; + } + log.info("Accept WebSocket handshake: HTTP session binding verified"); attributes.put("token", token); response.getHeaders().put("Sec-WebSocket-Protocol", protocols); return true; diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/controller/AuthController.java b/backend/web/src/main/java/org/jumpserver/chen/web/controller/AuthController.java index 6e6691fe..34ebd361 100644 --- a/backend/web/src/main/java/org/jumpserver/chen/web/controller/AuthController.java +++ b/backend/web/src/main/java/org/jumpserver/chen/web/controller/AuthController.java @@ -32,6 +32,8 @@ public AuthResponse auth(HttpServletRequest request, @RequestBody AuthRequest au var lang = getLanguage(request); sess.setLocale(lang); + // Chen token 是 bearer token;记录所属浏览器 HTTP session,供后续 WS 握手校验。 + sess.setAttribute(SessionManager.WEB_SESSION_ID_ATTRIBUTE, request.getSession().getId()); var chenToken = SessionManager.registerSession(sess); return new AuthResponse(chenToken, lang.toLanguageTag()); diff --git a/backend/web/src/main/java/org/jumpserver/chen/web/controller/ConsoleController.java b/backend/web/src/main/java/org/jumpserver/chen/web/controller/ConsoleController.java index b936c015..d02a7368 100644 --- a/backend/web/src/main/java/org/jumpserver/chen/web/controller/ConsoleController.java +++ b/backend/web/src/main/java/org/jumpserver/chen/web/controller/ConsoleController.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -25,8 +26,18 @@ public ResponseEntity exportData(@PathVariable String fileKey) { if (!SessionManager.getCurrentSession().canDownload()) { throw new ChenException(MessageUtils.get("NoPermissionError")); } - var path = SessionManager.getCurrentSession().getTempPath(); - Resource resource = new FileSystemResource(path.resolve(fileKey).toFile()); + // export 文件名只能是单个文件名,先拒绝跨平台的路径分隔符和遍历片段。 + if (fileKey == null || fileKey.isBlank() + || fileKey.contains("/") || fileKey.contains("\\") || fileKey.contains("..")) { + throw new ChenException("Invalid export file"); + } + var basePath = SessionManager.getCurrentSession().getTempPath().toAbsolutePath().normalize(); + var filePath = basePath.resolve(fileKey).normalize(); + // normalize 后仍须落在会话目录内,且禁止末级符号链接指向目录外文件。 + if (!filePath.startsWith(basePath) || !Files.isRegularFile(filePath, LinkOption.NOFOLLOW_LINKS)) { + throw new ChenException("Invalid export file"); + } + Resource resource = new FileSystemResource(filePath.toFile()); var resp = ResponseEntity .ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) From f12ec1e2802d213be431c428db6515f71af49e12 Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Wed, 15 Jul 2026 15:23:00 +0800 Subject: [PATCH 06/21] fix: validate database identifier before JDBC URL interpolation --- .../base/BaseConnectionManager.java | 4 ++ .../framework/utils/SqlIdentifierUtils.java | 40 +++++++++++++++++++ .../postgresql/PostgresqlSQLHintsHandler.java | 4 ++ 3 files changed, 48 insertions(+) create mode 100644 backend/framework/src/main/java/org/jumpserver/chen/framework/utils/SqlIdentifierUtils.java diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java index 577a3699..b3d1dfce 100644 --- a/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/base/BaseConnectionManager.java @@ -10,6 +10,7 @@ import org.jumpserver.chen.framework.driver.DriverClassLoader; import org.jumpserver.chen.framework.driver.DriverManager; import org.jumpserver.chen.framework.i18n.MessageUtils; +import org.jumpserver.chen.framework.utils.SqlIdentifierUtils; import java.lang.reflect.InvocationTargetException; import java.sql.Connection; @@ -120,6 +121,9 @@ public DruidDataSource getOrInitDataSource(String database) throws SQLException if (StringUtils.isEmpty(database)) { database = this.connectInfo.getDb(); } + // Reject URL metacharacters before interpolation into the JDBC URL, + // preventing injection of driver connection properties. + SqlIdentifierUtils.validateDatabaseName(database); if (this.dataSourceMap.containsKey(database)) { return this.dataSourceMap.get(database); } diff --git a/backend/framework/src/main/java/org/jumpserver/chen/framework/utils/SqlIdentifierUtils.java b/backend/framework/src/main/java/org/jumpserver/chen/framework/utils/SqlIdentifierUtils.java new file mode 100644 index 00000000..a68270eb --- /dev/null +++ b/backend/framework/src/main/java/org/jumpserver/chen/framework/utils/SqlIdentifierUtils.java @@ -0,0 +1,40 @@ +package org.jumpserver.chen.framework.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.sql.SQLException; + +/** + * Validates runtime database identifiers before they are interpolated into a + * JDBC URL, so that URL/property metacharacters in a database name cannot + * inject driver connection properties (e.g. PostgreSQL socketFactory/loggerFile, + * SQLServer ";prop=val"). + */ +@Slf4j +public final class SqlIdentifierUtils { + + // Characters that act as delimiters in at least one supported JDBC URL form. + // Rejecting them keeps a database name from escaping its ${db} placeholder. + private static final String FORBIDDEN_CHARS = "?&/:@#\\;= \t\r\n"; + + private SqlIdentifierUtils() { + } + + /** + * Reject database names containing URL/property metacharacters or control + * characters. Blank values are allowed; callers fall back to the configured db. + */ + public static void validateDatabaseName(String name) throws SQLException { + if (name == null || name.isEmpty()) { + return; + } + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (c < 0x20 || FORBIDDEN_CHARS.indexOf(c) >= 0) { + // Do not echo the value: it may contain log-forging characters. + log.warn("Rejected database name containing URL metacharacter"); + throw new SQLException("Invalid database identifier"); + } + } + } +} diff --git a/backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlSQLHintsHandler.java b/backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlSQLHintsHandler.java index 38be4baa..c0b970dc 100644 --- a/backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlSQLHintsHandler.java +++ b/backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlSQLHintsHandler.java @@ -6,6 +6,7 @@ import org.jumpserver.chen.framework.datasource.entity.resource.Field; import org.jumpserver.chen.framework.datasource.entity.resource.Table; import org.jumpserver.chen.framework.datasource.sql.SQL; +import org.jumpserver.chen.framework.utils.SqlIdentifierUtils; import org.jumpserver.chen.framework.utils.TreeUtils; import java.sql.SQLException; @@ -46,6 +47,9 @@ public Map> getHints(String nodeKey, String context) throws var db = TreeUtils.getValue(nodeKey, "database"); if (StringUtils.isNotEmpty(db)) { + // nodeKey is client-controlled; reject URL metacharacters before it + // reaches the JDBC URL via setDatabaseContext. + SqlIdentifierUtils.validateDatabaseName(db); this.connectionManager.setDatabaseContext(db); } From 791362abb71efc007d4c3c6424e4e073aaf5f0f1 Mon Sep 17 00:00:00 2001 From: "Crane.z" <1481445951@qq.com> Date: Mon, 20 Jul 2026 15:59:13 +0800 Subject: [PATCH 07/21] feat(frontend): add Dameng SQL formatter and editor fallbacks --- .../Main/Explore/Dialog/FormTemplate.vue | 28 +--------- .../Main/Explore/QueryConsole/CodeEditor.vue | 31 ++--------- frontend/src/utils/sqlEditorSupport.js | 55 +++++++++++++++++++ 3 files changed, 64 insertions(+), 50 deletions(-) create mode 100644 frontend/src/utils/sqlEditorSupport.js diff --git a/frontend/src/components/Main/Explore/Dialog/FormTemplate.vue b/frontend/src/components/Main/Explore/Dialog/FormTemplate.vue index 9d9ea97a..ebd2f98a 100644 --- a/frontend/src/components/Main/Explore/Dialog/FormTemplate.vue +++ b/frontend/src/components/Main/Explore/Dialog/FormTemplate.vue @@ -52,28 +52,7 @@ import { compileSQL } from '@/utils/sql' import 'codemirror/theme/3024-night.css' import 'codemirror/mode/sql/sql.js' import store from '@/store' -import { format } from 'sql-formatter' - -const formatterMap = { - null: 'sql', - 'mariadb': 'mariadb', - 'mysql': 'mysql', - 'postgresql': 'postgresql', - 'oracle': 'plsql', - 'db2': 'db2', - 'dameng': 'dameng' -} - -const modeMap = { - null: 'text/x-sql', - 'mariadb': 'text/x-mariadb', - 'mysql': 'text/x-mysql', - 'postgresql': 'text/x-pgsql', - 'oracle': 'text/x-plsql', - 'sqlserver': 'text/x-mssql', - 'db2': 'text/x-sql', - 'dameng': 'text/x-sql' -} +import { formatSqlForEditor, getEditorMode } from '@/utils/sqlEditorSupport' export default { props: { @@ -102,7 +81,7 @@ export default { lineNumbers: true, line: true, readOnly: true, - mode: modeMap[store.getters.profile?.dbType], + mode: getEditorMode(store.getters.profile?.dbType), cursorBlinkRate: -1 } } @@ -132,8 +111,7 @@ export default { const sql = compileSQL( this.formMeta.sqlTemplate, this.sqlParams ) - const lang = formatterMap[store.getters.profile?.dbType] - return format(sql, { language: lang }) + return formatSqlForEditor(sql, store.getters.profile?.dbType) }, set() {} } diff --git a/frontend/src/components/Main/Explore/QueryConsole/CodeEditor.vue b/frontend/src/components/Main/Explore/QueryConsole/CodeEditor.vue index 76adcaa2..cf9bead6 100644 --- a/frontend/src/components/Main/Explore/QueryConsole/CodeEditor.vue +++ b/frontend/src/components/Main/Explore/QueryConsole/CodeEditor.vue @@ -46,10 +46,10 @@