Skip to content

Commit ec53bf1

Browse files
authored
Merge pull request #179 from jumpserver/pr@new_terminal@merge_dev_new-terminal
Merge remote-tracking branch 'origin/dev' into new_terminal
2 parents c68bb54 + 5ebe341 commit ec53bf1

11 files changed

Lines changed: 167 additions & 23 deletions

File tree

backend/framework/src/main/java/org/jumpserver/chen/framework/console/QueryConsole.java

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
import java.sql.Connection;
3333
import java.sql.SQLException;
3434
import java.util.ArrayList;
35+
import java.util.Collections;
3536
import java.util.HashMap;
37+
import java.util.LinkedHashMap;
38+
import java.util.List;
3639
import java.util.Map;
3740
import java.util.concurrent.ConcurrentHashMap;
3841
import java.util.concurrent.CountDownLatch;
@@ -47,6 +50,7 @@ public class QueryConsole extends AbstractConsole {
4750
private volatile SQLExecutePlan currentPlan;
4851
private StateManager<QueryConsoleState> stateManager;
4952
private final Map<String, DataView> dataViews = new HashMap<>();
53+
private volatile Map<String, String> allowedContexts = Map.of();
5054

5155
private static final Gson GSON = new Gson();
5256

@@ -99,6 +103,7 @@ public void onConnect(Connect connect) {
99103
}
100104

101105
var schemas = this.getSqlActuator().getSchemas();
106+
this.replaceAllowedContexts(schemas);
102107
this.getState().setContexts(schemas);
103108

104109
} catch (SQLException e) {
@@ -285,15 +290,19 @@ public void onCancel() {
285290
}
286291

287292
public void onManualChangeContext(String context) {
288-
if (StringUtils.equals(this.getState().getCurrentContext(), context)) {
293+
if (!this.isAllowedContext(context)) {
294+
return;
295+
}
296+
var allowedContext = this.allowedContexts.get(context);
297+
if (StringUtils.equals(this.getState().getCurrentContext(), allowedContext)) {
289298
return;
290299
}
291300
try {
292301
this.getState().setEditorLoading(true);
293302
this.stateManager.commit();
294303

295-
this.getSqlActuator().changeSchema(context);
296-
this.getState().setCurrentContext(context);
304+
this.getSqlActuator().changeSchema(allowedContext);
305+
this.getState().setCurrentContext(allowedContext);
297306

298307
} catch (SQLException e) {
299308
this.getConsoleLogger().error(MessageUtils.get("ChangeContextError") + ": %s", e.getMessage());
@@ -304,8 +313,30 @@ public void onManualChangeContext(String context) {
304313

305314
}
306315

316+
void replaceAllowedContexts(List<String> contexts) {
317+
var canonicalContexts = new LinkedHashMap<String, String>();
318+
if (contexts != null) {
319+
for (String context : contexts) {
320+
if (StringUtils.isNotBlank(context)) {
321+
canonicalContexts.putIfAbsent(context, context);
322+
}
323+
}
324+
}
325+
this.allowedContexts = Collections.unmodifiableMap(canonicalContexts);
326+
}
327+
328+
boolean isAllowedContext(String context) {
329+
return StringUtils.isNotBlank(context) && this.allowedContexts.containsKey(context);
330+
}
307331

308332
public void onSQLFile(String filename) {
333+
if (StringUtils.isBlank(filename)
334+
|| filename.contains("/")
335+
|| filename.contains("\\")
336+
|| filename.contains("..")) {
337+
log.warn("Rejected invalid SQL file name");
338+
return;
339+
}
309340
var filePath = SessionManager.getCurrentSession().getTempPath().resolve(filename);
310341
var file = filePath.toFile();
311342

backend/framework/src/main/java/org/jumpserver/chen/framework/datasource/sql/SQL.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ public SQL(String sql) {
1919
this.sql = sql;
2020
}
2121

22-
public static SQL of(String sql, Map<String, Object> params) {
22+
/**
23+
* Performs raw string interpolation. This is not a PreparedStatement.
24+
* Never pass user-controlled values or SQL identifiers.
25+
*/
26+
public static SQL unsafeInterpolate(String sql, Map<String, Object> params) {
2327
List<String> paramNames = new ArrayList<>();
2428
Matcher matcher = Pattern.compile(":(\\w+)").matcher(sql);
2529
while (matcher.find()) {
@@ -35,7 +39,11 @@ public static SQL of(String sql, Map<String, Object> params) {
3539
return new SQL(sql);
3640
}
3741

38-
public static SQL of(String sql, Object... params) {
42+
/**
43+
* Performs raw string interpolation. This is not a PreparedStatement.
44+
* Never pass user-controlled values or SQL identifiers.
45+
*/
46+
public static SQL unsafeInterpolate(String sql, Object... params) {
3947
StringBuilder sb = new StringBuilder();
4048
int lastPos = 0;
4149
for (Object param : params) {
@@ -49,6 +57,24 @@ public static SQL of(String sql, Object... params) {
4957
return new SQL(sb.toString());
5058
}
5159

60+
/**
61+
* @deprecated This method performs unsafe raw string interpolation. Use a
62+
* PreparedStatement for values and dialect-specific quoting for identifiers.
63+
*/
64+
@Deprecated
65+
public static SQL of(String sql, Map<String, Object> params) {
66+
return unsafeInterpolate(sql, params);
67+
}
68+
69+
/**
70+
* @deprecated This method performs unsafe raw string interpolation. Use a
71+
* PreparedStatement for values and dialect-specific quoting for identifiers.
72+
*/
73+
@Deprecated
74+
public static SQL of(String sql, Object... params) {
75+
return unsafeInterpolate(sql, params);
76+
}
77+
5278
public static SQL of(String sql) {
5379
return new SQL(sql);
5480
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.jumpserver.chen.framework.datasource.sql;
2+
3+
import com.alibaba.druid.DbType;
4+
import org.apache.commons.lang3.StringUtils;
5+
6+
public final class SQLIdentifier {
7+
private SQLIdentifier() {
8+
}
9+
10+
public static String quote(DbType dbType, String identifier) {
11+
if (StringUtils.isBlank(identifier)) {
12+
throw new IllegalArgumentException("SQL identifier must not be blank");
13+
}
14+
for (int i = 0; i < identifier.length(); i++) {
15+
if (Character.isISOControl(identifier.charAt(i))) {
16+
throw new IllegalArgumentException("SQL identifier must not contain control characters");
17+
}
18+
}
19+
20+
return switch (dbType) {
21+
case mysql, mariadb, clickhouse ->
22+
"`" + identifier.replace("`", "``") + "`";
23+
case sqlserver ->
24+
"[" + identifier.replace("]", "]]") + "]";
25+
case postgresql, oracle, db2, dm ->
26+
"\"" + identifier.replace("\"", "\"\"") + "\"";
27+
default -> throw new IllegalArgumentException("Unsupported database type: " + dbType);
28+
};
29+
}
30+
}

backend/framework/src/main/java/org/jumpserver/chen/framework/ws/ConsoleWebSocketHandler.java

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,32 @@
77
import org.jumpserver.chen.framework.console.DataViewConsole;
88
import org.jumpserver.chen.framework.console.QueryConsole;
99
import org.jumpserver.chen.framework.console.entity.request.Connect;
10+
import org.jumpserver.chen.framework.datasource.ResourceBrowser;
11+
import org.jumpserver.chen.framework.datasource.entity.resource.TreeNode;
1012
import org.jumpserver.chen.framework.session.SessionManager;
11-
import org.jumpserver.chen.framework.utils.TreeUtils;
13+
import org.jumpserver.chen.framework.session.controller.message.Message;
14+
import org.jumpserver.chen.framework.session.controller.message.MessageLevel;
1215
import org.jumpserver.chen.framework.ws.io.Packet;
16+
import org.jumpserver.chen.framework.ws.io.PacketIO;
17+
import org.jumpserver.chen.framework.utils.TreeUtils;
1318
import org.springframework.web.socket.CloseStatus;
1419
import org.springframework.web.socket.WebSocketMessage;
1520
import org.springframework.web.socket.WebSocketSession;
1621
import org.springframework.web.socket.adapter.NativeWebSocketSession;
1722
import org.springframework.web.socket.handler.TextWebSocketHandler;
1823

24+
import java.sql.SQLException;
25+
import java.util.Set;
1926
import java.util.concurrent.ExecutorService;
2027
import java.util.concurrent.Executors;
2128

2229
@Slf4j
2330
public class ConsoleWebSocketHandler extends TextWebSocketHandler {
2431

2532
private static final Gson GSON = new Gson();
33+
private static final Set<String> QUERY_NODE_TYPES =
34+
Set.of("datasource", "database", "schema", "table");
35+
private static final Set<String> DATA_VIEW_NODE_TYPES = Set.of("table", "view");
2636

2737

2838
@Override
@@ -71,17 +81,28 @@ public void handleMessage(WebSocketSession session, WebSocketMessage<?> message)
7181

7282
private void onConnectPacket(WebSocketSession session, Packet packet) {
7383
Connect connect = GSON.fromJson(GSON.toJson(packet.getData()), Connect.class);
74-
Console console = null;
7584
var webSess = SessionManager.getCurrentSession();
76-
77-
switch (connect.getType()) {
78-
case Connect.CONSOLE_TYPE_QUERY -> {
79-
console = new QueryConsole(webSess.getDatasource(), session, connect.getNodeKey());
80-
}
81-
case Connect.CONSOLE_TYPE_DATA_VIEW -> {
82-
console = new DataViewConsole(webSess.getDatasource(), session, connect.getNodeKey());
83-
}
85+
var node = this.resolveNode(
86+
webSess.getDatasource().getResourceBrowser(),
87+
connect.getNodeKey(),
88+
connect.getType()
89+
);
90+
if (node == null) {
91+
new PacketIO(session).sendPacket(
92+
"show_message",
93+
new Message(MessageLevel.ERROR, "Invalid console context")
94+
);
95+
return;
8496
}
97+
98+
connect.setNodeKey(node.getKey());
99+
Console console = switch (connect.getType()) {
100+
case Connect.CONSOLE_TYPE_QUERY ->
101+
new QueryConsole(webSess.getDatasource(), session, node.getKey());
102+
case Connect.CONSOLE_TYPE_DATA_VIEW ->
103+
new DataViewConsole(webSess.getDatasource(), session, node.getKey());
104+
default -> null;
105+
};
85106
if (console != null) {
86107
webSess.getConsoles().put(session.getId(), console);
87108

@@ -93,6 +114,27 @@ private void onConnectPacket(WebSocketSession session, Packet packet) {
93114
}
94115
}
95116

117+
TreeNode resolveNode(ResourceBrowser resourceBrowser, String nodeKey, String consoleType) {
118+
if (resourceBrowser == null || StringUtils.isBlank(nodeKey) || StringUtils.isBlank(consoleType)) {
119+
return null;
120+
}
121+
TreeNode node;
122+
try {
123+
var root = resourceBrowser.getTree();
124+
node = root == null ? null : TreeUtils.getNode(root, nodeKey);
125+
} catch (SQLException e) {
126+
return null;
127+
}
128+
if (node == null) {
129+
return null;
130+
}
131+
var allowedTypes = switch (consoleType) {
132+
case Connect.CONSOLE_TYPE_QUERY -> QUERY_NODE_TYPES;
133+
case Connect.CONSOLE_TYPE_DATA_VIEW -> DATA_VIEW_NODE_TYPES;
134+
default -> Set.<String>of();
135+
};
136+
return allowedTypes.contains(node.getType()) ? node : null;
137+
}
96138

97139
@Override
98140
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
@@ -111,7 +153,9 @@ public void afterConnectionClosed(WebSocketSession session, CloseStatus closeSta
111153
.getCurrentSession()
112154
.getConsoles()
113155
.get(session.getId());
114-
console.close();
156+
if (console != null) {
157+
console.close();
158+
}
115159
SessionManager.getCurrentSession().getConsoles().remove(session.getId());
116160
}
117161
}

backend/modules/src/main/java/org.jumpserver.chen.modules/clickhouse/ClickhouseActuator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.jumpserver.chen.framework.datasource.base.BaseSQLActuator;
55
import org.jumpserver.chen.framework.datasource.sql.SQL;
66
import org.jumpserver.chen.framework.datasource.sql.SQLExecutePlan;
7+
import org.jumpserver.chen.framework.datasource.sql.SQLIdentifier;
78
import org.jumpserver.chen.framework.datasource.sql.SQLQueryParams;
89

910
import java.sql.Connection;
@@ -33,7 +34,8 @@ public List<String> getSchemas() throws SQLException {
3334

3435
@Override
3536
public void changeSchema(String schema) throws SQLException {
36-
this.execute(SQL.of("use `?`", schema));
37+
var identifier = SQLIdentifier.quote(this.getDbType(), schema);
38+
this.execute(SQL.of("use " + identifier));
3739
}
3840

3941

backend/modules/src/main/java/org.jumpserver.chen.modules/dameng/DMActuator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.jumpserver.chen.framework.datasource.base.BaseSQLActuator;
55
import org.jumpserver.chen.framework.datasource.sql.SQL;
66
import org.jumpserver.chen.framework.datasource.sql.SQLExecutePlan;
7+
import org.jumpserver.chen.framework.datasource.sql.SQLIdentifier;
78
import org.jumpserver.chen.framework.datasource.sql.SQLQueryParams;
89

910
import java.sql.Connection;
@@ -33,7 +34,8 @@ public List<String> getSchemas() throws SQLException {
3334

3435
@Override
3536
public void changeSchema(String schema) throws SQLException {
36-
this.execute(SQL.of("set schema ?", schema));
37+
var identifier = SQLIdentifier.quote(this.getDbType(), schema);
38+
this.execute(SQL.of("set schema " + identifier));
3739
}
3840

3941

backend/modules/src/main/java/org.jumpserver.chen.modules/db2/DB2Actuator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.jumpserver.chen.framework.datasource.base.BaseSQLActuator;
55
import org.jumpserver.chen.framework.datasource.sql.SQL;
66
import org.jumpserver.chen.framework.datasource.sql.SQLExecutePlan;
7+
import org.jumpserver.chen.framework.datasource.sql.SQLIdentifier;
78
import org.jumpserver.chen.framework.datasource.sql.SQLQueryParams;
89

910
import java.sql.Connection;
@@ -33,7 +34,8 @@ public List<String> getSchemas() throws SQLException {
3334

3435
@Override
3536
public void changeSchema(String schema) throws SQLException {
36-
this.execute(SQL.of("set current schema ?", schema));
37+
var identifier = SQLIdentifier.quote(this.getDbType(), schema);
38+
this.execute(SQL.of("set current schema " + identifier));
3739
}
3840

3941

backend/modules/src/main/java/org.jumpserver.chen.modules/mysql/MysqlActuator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public List<String> getSchemas() throws SQLException {
3131

3232
@Override
3333
public void changeSchema(String schema) throws SQLException {
34-
this.execute(SQL.of("use `?`", schema));
34+
var identifier = SQLIdentifier.quote(this.getDbType(), schema);
35+
this.execute(SQL.of("use " + identifier));
3536
}
3637

3738

backend/modules/src/main/java/org.jumpserver.chen.modules/oracle/OracleActuator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.jumpserver.chen.framework.datasource.base.BaseSQLActuator;
77
import org.jumpserver.chen.framework.datasource.sql.SQL;
88
import org.jumpserver.chen.framework.datasource.sql.SQLExecutePlan;
9+
import org.jumpserver.chen.framework.datasource.sql.SQLIdentifier;
910
import org.jumpserver.chen.framework.datasource.sql.SQLQueryParams;
1011

1112
import java.sql.Connection;
@@ -35,7 +36,8 @@ public List<String> getSchemas() throws SQLException {
3536

3637
@Override
3738
public void changeSchema(String schema) throws SQLException {
38-
this.execute(SQL.of("ALTER SESSION SET CURRENT_SCHEMA = ?", schema));
39+
var identifier = SQLIdentifier.quote(this.getDbType(), schema);
40+
this.execute(SQL.of("ALTER SESSION SET CURRENT_SCHEMA = " + identifier));
3941
}
4042

4143
@Override

backend/modules/src/main/java/org.jumpserver.chen.modules/postgresql/PostgresqlActuator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.jumpserver.chen.framework.datasource.base.BaseSQLActuator;
66
import org.jumpserver.chen.framework.datasource.sql.SQL;
77
import org.jumpserver.chen.framework.datasource.sql.SQLExecutePlan;
8+
import org.jumpserver.chen.framework.datasource.sql.SQLIdentifier;
89
import org.jumpserver.chen.framework.datasource.sql.SQLQueryParams;
910

1011
import java.sql.Connection;
@@ -38,7 +39,8 @@ public List<String> getSchemas() throws SQLException {
3839
public void changeSchema(String schema) throws SQLException {
3940
var ss = schema.split("\\.");
4041
var schemaName = ss.length > 1 ? ss[1] : ss[0];
41-
this.execute(SQL.of("SET SEARCH_PATH TO '?';", schemaName));
42+
var identifier = SQLIdentifier.quote(this.getDbType(), schemaName);
43+
this.execute(SQL.of("SET SEARCH_PATH TO " + identifier));
4244
}
4345

4446
@Override

0 commit comments

Comments
 (0)