Compare commits

...
Author SHA1 Message Date
aias00 0328bbfba0 Merge branch 'master' into security/fix_maven_security 2025-05-18 20:41:47 +08:00
62cf3d1b2b [improve] improve jndi validation (#3358)
Signed-off-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: tomsun28 <tomsun28@outlook.com>
Co-authored-by: Calvin <naruse_shinji@163.com>
2025-05-18 17:45:09 +08:00
aias00 452ebf8032 Merge branch 'master' into security/fix_maven_security 2025-05-18 17:07:01 +08:00
Calvin 3ae80dfb89 Merge branch 'master' into security/fix_maven_security 2025-05-18 13:47:33 +08:00
liuhy c375ff5c88 fix maven dep security 2025-05-17 13:40:12 +08:00
4 changed files with 105 additions and 22 deletions
@@ -69,8 +69,8 @@
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>provided</scope>
</dependency>
<!-- clickhouse -->
@@ -66,7 +66,7 @@ public class JmxCollectImpl extends AbstractCollect {
private static final String JMX_URL_PREFIX = "service:jmx:rmi:///jndi/rmi://";
private static final String JMX_URL_SUFFIX = "/jmxrmi";
private static final String IGNORED_STUB = "/stub/";
private static final String SUB_ATTRIBUTE = "->";
@@ -75,7 +75,6 @@ public class JmxCollectImpl extends AbstractCollect {
private final ClassLoader jmxClassLoader;
public JmxCollectImpl() {
jmxClassLoader = new JmxClassLoader(ClassLoader.getSystemClassLoader());
}
@@ -83,13 +82,66 @@ public class JmxCollectImpl extends AbstractCollect {
@Override
public void preCheck(Metrics metrics) throws IllegalArgumentException {
Assert.isTrue(metrics != null && metrics.getJmx() != null, "JMX collect must have JMX params");
JmxProtocol jmxProtocol = metrics.getJmx();
String url = metrics.getJmx().getUrl();
// Validate JMX URL if provided
String url = jmxProtocol.getUrl();
if (StringUtils.hasText(url)) {
Assert.doesNotContain(url, IGNORED_STUB, "JMX url prohibit contains stub, please check");
// Prevent JNDI injection by validating URL format
validateJmxUrl(url);
} else {
// Validate host and port inputs
String host = jmxProtocol.getHost();
int port = Integer.parseInt(jmxProtocol.getPort());
// Validate host format (only allow valid hostnames or IP addresses)
Assert.isTrue(isValidHostname(host), "Invalid hostname format");
Assert.isTrue(port > 0 && port <= 65535, "Port must be between 1 and 65535");
}
}
/**
* Validate JMX URL
*
* @param url JMX URL to validate
* @throws IllegalArgumentException if URL is potentially malicious
*/
private void validateJmxUrl(String url) throws IllegalArgumentException {
// Only allow service:jmx:rmi protocol
Assert.isTrue(url.startsWith("service:jmx:rmi:"), "Only service:jmx:rmi protocol is supported");
String[] disallowedPatterns = { "ldap:", "rmi:", "iiop:", "nis:", "dns:", "corbaname:", "http:", "https:" };
for (String pattern : disallowedPatterns) {
if (url.contains(pattern) && !pattern.equals("rmi:///jndi/rmi:")) {
throw new IllegalArgumentException("Potentially unsafe JNDI protocol detected in URL: " + pattern);
}
}
// Check for suspicious patterns
if (url.contains("${") || url.contains("$[") || url.contains(":#") || url.contains(":/")) {
throw new IllegalArgumentException("Potentially malicious pattern detected in JMX URL");
}
}
/**
* Validate hostname format
*
* @param hostname Hostname to validate
* @return true if hostname is valid
*/
private boolean isValidHostname(String hostname) {
if (hostname == null || hostname.isEmpty()) {
return false;
}
// Simplified hostname/IP validation regex
// This regex accepts valid hostnames, IPv4 and IPv6 addresses
String hostnameRegex = "^([a-zA-Z0-9][-a-zA-Z0-9]*\\.)+[a-zA-Z0-9][-a-zA-Z0-9]*$|^(\\d{1,3}\\.){3}\\d{1,3}$|^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$";
return hostname.matches(hostnameRegex);
}
@Override
public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
@@ -172,16 +224,18 @@ public class JmxCollectImpl extends AbstractCollect {
log.info("attribute {} value is null.", attribute.getName());
continue;
}
if (value instanceof Number || value instanceof String || value instanceof ObjectName
if (value instanceof Number || value instanceof String || value instanceof ObjectName
|| value instanceof Boolean || value instanceof Date || value instanceof TimeUnit) {
attributeValueMap.put(attribute.getName(), value.toString());
} else if (value instanceof CompositeData compositeData) {
} else if (value instanceof CompositeData) {
CompositeData compositeData = (CompositeData) value;
CompositeType compositeType = compositeData.getCompositeType();
for (String typeKey : compositeType.keySet()) {
Object fieldValue = compositeData.get(typeKey);
attributeValueMap.put(attribute.getName() + SUB_ATTRIBUTE + typeKey, fieldValue.toString());
}
} else if (value instanceof String[] values) {
} else if (value instanceof String[]) {
String[] values = (String[]) value;
StringBuilder builder = new StringBuilder();
for (int index = 0; index < values.length; index++) {
builder.append(values[index]);
@@ -219,12 +273,33 @@ public class JmxCollectImpl extends AbstractCollect {
String url;
if (jmxProtocol.getUrl() != null) {
url = jmxProtocol.getUrl();
// Double check URL format for security
if (!url.startsWith("service:jmx:rmi:")) {
throw new IOException("Unsupported JMX URL protocol. Only service:jmx:rmi: is allowed.");
}
} else {
url = JMX_URL_PREFIX + jmxProtocol.getHost() + ":" + jmxProtocol.getPort() + JMX_URL_SUFFIX;
// More strict formatting with proper escaping
String host = jmxProtocol.getHost();
int port = Integer.parseInt(jmxProtocol.getPort());
// Additional validation at connection time
if (!isValidHostname(host)) {
throw new IOException("Invalid hostname format for JMX connection: " + host);
}
if (port <= 0 || port > 65535) {
throw new IOException("Invalid port for JMX connection: " + port);
}
url = JMX_URL_PREFIX + host + ":" + port + JMX_URL_SUFFIX;
}
// Set security properties to prevent remote class loading
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false");
Map<String, Object> environment = new HashMap<>(4);
if (StringUtils.hasText(jmxProtocol.getUsername()) && StringUtils.hasText(jmxProtocol.getPassword())) {
String[] credential = new String[] {jmxProtocol.getUsername(), jmxProtocol.getPassword()};
String[] credential = new String[] { jmxProtocol.getUsername(), jmxProtocol.getPassword() };
environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credential);
}
if (Boolean.TRUE.toString().equals(jmxProtocol.getSsl())) {
@@ -233,10 +308,20 @@ public class JmxCollectImpl extends AbstractCollect {
environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
}
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
// Limit JMX connection timeout
environment.put("jmx.remote.x.client.connection.timeout", 10000);
environment.put("jmx.remote.x.server.connection.timeout", 10000);
try {
JMXServiceURL jmxServiceUrl = new JMXServiceURL(url);
conn = JMXConnectorFactory.connect(jmxServiceUrl, environment);
connectionCommonCache.addCache(identifier, new JmxConnect(conn));
return conn;
} catch (Exception e) {
log.error("Failed to connect to JMX server: {}", e.getMessage());
throw new IOException("Failed to connect to JMX server: " + e.getMessage(), e);
}
}
}
+2 -2
View File
@@ -150,8 +150,8 @@
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>provided</scope>
</dependency>
<!-- email -->
+4 -6
View File
@@ -128,9 +128,9 @@
<xml.bind.version>2.3.0</xml.bind.version>
<snake.yaml.version>2.2</snake.yaml.version>
<okhttp.version>4.12.0</okhttp.version>
<kafka-clients.version>3.4.0</kafka-clients.version>
<kafka-clients.version>3.7.1</kafka-clients.version>
<netty.version>4.1.117.Final</netty.version>
<mysql.version>8.0.30</mysql.version>
<mysql.version>8.4.0</mysql.version>
<mssql-jdbc.version>10.2.0.jre8</mssql-jdbc.version>
<ojdbc8.version>21.5.0.0</ojdbc8.version>
<mongodb-driver.version>4.6.1</mongodb-driver.version>
@@ -164,14 +164,12 @@
<spring-boot-starter-sureness.version>1.1.0</spring-boot-starter-sureness.version>
<huawei.sdk.version>3.1.37</huawei.sdk.version>
<huawei.obs.version>3.23.5</huawei.obs.version>
<commons-net>3.8.0</commons-net>
<iotdb-session.version>0.13.3</iotdb-session.version>
<influxdb.version>2.23</influxdb.version>
<spring-cloud-starter-openfeign.version>3.0.5</spring-cloud-starter-openfeign.version>
<taos-jdbcdriver.version>3.0.0</taos-jdbcdriver.version>
<greptimedb.version>0.11.0</greptimedb.version>
<mysql-jdbcdriver.version>8.0.33</mysql-jdbcdriver.version>
<arrow.version>18.1.0</arrow.version>
<snappy-java.version>1.1.10.7</snappy-java.version>
<sshd-sftp.version>2.13.1</sshd-sftp.version>
@@ -324,8 +322,8 @@
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- sql server -->