fix: mitigate SnakeYAML deserialization RCE and enforce default password change

1. Use SafeConstructor in ApplicationConfigMonitor to block arbitrary
   object instantiation via YAML type tags (e.g. !!javax.script.ScriptEngineManager).
2. Detect default admin password (admin/123456) on login and enforce
   password change before allowing any other API operations (code 50015).

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
jianghang.loujh
2026-04-29 18:07:02 +08:00
parent ac6242bae9
commit cf97b2ae31
5 changed files with 50 additions and 2 deletions

View File

@@ -98,6 +98,26 @@ public class WebConfig implements WebMvcConfigurer {
return false; return false;
} }
// 检查是否使用默认密码,如果是则仅允许改密和登出操作
User validUser = (User) httpServletRequest.getAttribute("user");
if (validUser != null && validUser.isDefaultPassword()) {
String uri = httpServletRequest.getRequestURI();
String method = httpServletRequest.getMethod();
boolean isPasswordChange = "PUT".equals(method) && uri.matches("/api/[^/]+/user");
boolean isLogout = uri.contains("/user/logout");
if (!isPasswordChange && !isLogout) {
BaseModel baseModel = BaseModel.getInstance(null);
baseModel.setCode(50015);
baseModel.setMessage("Default password must be changed before proceeding");
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(baseModel);
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.print(json);
return false;
}
}
return true; return true;
} }
}) })

View File

@@ -56,6 +56,9 @@ public class UserController {
String token = UUID.randomUUID().toString(); String token = UUID.randomUUID().toString();
loginUsers.put(token, loginUser); loginUsers.put(token, loginUser);
tokenResp.put("token", token); tokenResp.put("token", token);
if (loginUser.isDefaultPassword()) {
tokenResp.put("needPasswordChange", "true");
}
return BaseModel.getInstance(tokenResp); return BaseModel.getInstance(tokenResp);
} else { } else {
BaseModel<Map<String, String>> model = BaseModel.getInstance(null); BaseModel<Map<String, String>> model = BaseModel.getInstance(null);
@@ -98,6 +101,7 @@ public class UserController {
HttpServletRequest httpServletRequest) { HttpServletRequest httpServletRequest) {
userService.update(user); userService.update(user);
String token = (String) httpServletRequest.getAttribute("token"); String token = (String) httpServletRequest.getAttribute("token");
user.setDefaultPassword(false);
loginUsers.put(token, user); loginUsers.put(token, user);
return BaseModel.getInstance("success"); return BaseModel.getInstance("success");
} }

View File

@@ -44,6 +44,9 @@ public class User extends Model {
@Transient @Transient
private String oldPassword; private String oldPassword;
@Transient
private boolean defaultPassword;
public Long getId() { public Long getId() {
return id; return id;
} }
@@ -115,4 +118,12 @@ public class User extends Model {
public void setOldPassword(String oldPassword) { public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword; this.oldPassword = oldPassword;
} }
public boolean isDefaultPassword() {
return defaultPassword;
}
public void setDefaultPassword(boolean defaultPassword) {
this.defaultPassword = defaultPassword;
}
} }

View File

@@ -3,6 +3,8 @@ package com.alibaba.otter.canal.admin.service.impl;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.alibaba.otter.canal.admin.common.exception.ServiceException; import com.alibaba.otter.canal.admin.common.exception.ServiceException;
@@ -19,6 +21,10 @@ import com.alibaba.otter.canal.protocol.SecurityUtil;
@Service @Service
public class UserServiceImpl implements UserService { public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
private static final String DEFAULT_PASSWORD_HASH = "6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9";
private static byte[] seeds = "canal is best!".getBytes(); private static byte[] seeds = "canal is best!".getBytes();
private static final Integer PASSWORD_LENGTH = 5; private static final Integer PASSWORD_LENGTH = 5;
@@ -40,6 +46,11 @@ public class UserServiceImpl implements UserService {
throw new ServiceException("user:" + user.getName() + " auth failed!"); throw new ServiceException("user:" + user.getName() + " auth failed!");
} }
if (DEFAULT_PASSWORD_HASH.equalsIgnoreCase(user.getPassword())) {
user.setDefaultPassword(true);
logger.warn("Default admin password detected for user: {}. Please change immediately.", username);
}
user.setPassword(""); user.setPassword("");
return user; return user;
} }

View File

@@ -18,7 +18,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import com.alibaba.otter.canal.adapter.launcher.loader.CanalAdapterService; import com.alibaba.otter.canal.adapter.launcher.loader.CanalAdapterService;
import com.alibaba.otter.canal.client.adapter.support.Util; import com.alibaba.otter.canal.client.adapter.support.Util;
@@ -70,7 +72,7 @@ public class ApplicationConfigMonitor {
super.onFileChange(file); super.onFileChange(file);
try { try {
// 检查yml格式 // 检查yml格式
new Yaml().loadAs(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8), Map.class); new Yaml(new SafeConstructor(new LoaderOptions())).loadAs(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8), Map.class);
canalAdapterService.destroy(); canalAdapterService.destroy();