修复漏洞

This commit is contained in:
zjf committed 2026-10-29 13:27:20 +08:00
1 parent 710e4cfb90
commit bb0b8bd7ef
14 files changed
+777 -161

No files matched your search

@@ -0,0 +1,57 @@
package com.bsd.pjcloud.common.core.util;
import cn.hutool.core.util.StrUtil;
/**
* 弱口令校验:禁止密码与手机号/用户名相同等低强度规则。
*/
public final class PasswordWeakCheckUtil {
private PasswordWeakCheckUtil() {
}
/**
* @param rawPassword 明文密码
* @param phone 手机号(可空)
* @param username 用户名(可空)
* @return 不通过时的中文原因;通过返回 null
*/
public static String validate(String rawPassword, String phone, String username) {
if (StrUtil.isBlank(rawPassword)) {
return "密码不能为空";
}
String pwd = StrUtil.trim(rawPassword);
String phoneTrim = StrUtil.trim(phone);
String userTrim = StrUtil.trim(username);
if (StrUtil.isNotBlank(phoneTrim) && pwd.equals(phoneTrim)) {
return "密码不能与手机号相同";
}
if (StrUtil.isNotBlank(userTrim) && pwd.equalsIgnoreCase(userTrim)) {
return "密码不能与用户名相同";
}
if (pwd.length() < 8) {
return "密码长度不能少于8位";
}
boolean hasLetter = false;
boolean hasDigit = false;
for (int i = 0; i < pwd.length(); i++) {
char c = pwd.charAt(i);
if (Character.isLetter(c)) {
hasLetter = true;
} else if (Character.isDigit(c)) {
hasDigit = true;
}
}
if (!hasLetter || !hasDigit) {
return "密码必须同时包含字母和数字";
}
return null;
}
public static void assertStrong(String rawPassword, String phone, String username) {
String err = validate(rawPassword, phone, username);
if (err != null) {
throw new IllegalArgumentException(err);
}
}
}
@@ -1,6 +1,6 @@
sys.user.update.passwordError=\u539F\u5BC6\u7801\u9519\u8BEF\uFF0C\u4FEE\u6539\u5931\u8D25
sys.user.query.error=\u83B7\u53D6\u5F53\u524D\u7528\u6237\u4FE1\u606F\u5931\u8D25
sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u4E3A\u624B\u673A\u53F7
sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u89C1\u7CFB\u7EDF\u53C2\u6570USER_DEFAULT_PASSWORD
sys.user.username.existing={0} \u7528\u6237\u540D\u5DF2\u5B58\u5728
sys.user.phone.existing={0} \u624b\u673a\u53f7\u5df2\u5b58\u5728
sys.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A
@@ -35,76 +35,107 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author pjcloud
* @date 2019-06-24
* <p>
* 解决Mybatis Plus Order By SQL注入问题
* 解决Mybatis Plus Order By SQL注入问题:ascs/descs 仅允许简单列名白名单,禁止表达式/函数注入。
*/
@Slf4j
public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 排序字段白名单:仅允许标识符(可选表别名.列名),禁止空格、括号、逗号、注释等。
* 例:create_time、user_id、u.createTime
*/
private static final Pattern SAFE_ORDER_COLUMN = Pattern.compile(
"^[a-zA-Z_][a-zA-Z0-9_]{0,63}(\\.[a-zA-Z_][a-zA-Z0-9_]{0,63})?$");
/**
* 判断Controller是否包含page 参数
*
* @param parameter 参数
* @return 是否过滤
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Page.class);
}
/** 单次请求最多允许的排序列数量,防止恶意堆叠。 */
private static final int MAX_ORDER_COLUMNS = 5;
/**
* @param parameter 入参集合
* @param mavContainer model 和 view
* @param webRequest web相关
* @param binderFactory 入参解析
* @return 检查后新的page对象
* <p>
* page 只支持查询 GET .如需解析POST获取请求报文体处理
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
/**
* 判断Controller是否包含page 参数
*
* @param parameter 参数
* @return 是否过滤
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Page.class);
}
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
/**
* @param parameter 入参集合
* @param mavContainer model 和 view
* @param webRequest web相关
* @param binderFactory 入参解析
* @return 检查后新的page对象
* <p>
* page 只支持查询 GET .如需解析POST获取请求报文体处理
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
String ascs = request.getParameter("ascs");
String descs = request.getParameter("descs");
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String current = request.getParameter("current");
String size = request.getParameter("size");
String ascs = request.getParameter("ascs");
String descs = request.getParameter("descs");
Page page = new Page();
if (StrUtil.isNotBlank(current)) {
// 如果current page 小于零 视为不合法数据
if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
throw new CheckedException("current page error");
}
page.setCurrent(Long.parseLong(current));
}
String current = request.getParameter("current");
String size = request.getParameter("size");
if (StrUtil.isNotBlank(size)) {
page.setSize(Long.parseLong(size));
}
Page page = new Page();
if (StrUtil.isNotBlank(current)) {
// 如果current page 小于零 视为不合法数据
if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
throw new CheckedException("current page error");
}
page.setCurrent(Long.parseLong(current));
}
List<OrderItem> orderItemList = new ArrayList<>();
Optional.ofNullable(ascs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(asc -> !SqlInjectionUtils.check(asc))
.map(OrderItem::asc)
.collect(Collectors.toList())));
Optional.ofNullable(descs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.filter(desc -> !SqlInjectionUtils.check(desc))
.map(OrderItem::desc)
.collect(Collectors.toList())));
page.addOrder(orderItemList);
if (StrUtil.isNotBlank(size)) {
page.setSize(Long.parseLong(size));
}
List<OrderItem> orderItemList = new ArrayList<>();
Optional.ofNullable(ascs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.map(col -> toSafeOrderColumn(col, true))
.collect(Collectors.toList())));
Optional.ofNullable(descs)
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.map(col -> toSafeOrderColumn(col, false))
.collect(Collectors.toList())));
if (orderItemList.size() > MAX_ORDER_COLUMNS) {
throw new CheckedException("排序字段数量超限");
}
page.addOrder(orderItemList);
return page;
}
/**
* 将前端排序字段转为安全的 OrderItem:必须通过标识符白名单,且通过 SqlInjectionUtils 黑名单兜底。
*/
private static OrderItem toSafeOrderColumn(String column, boolean asc) {
if (!SAFE_ORDER_COLUMN.matcher(column).matches()) {
log.warn("拒绝非法排序字段: {}", column);
throw new CheckedException("排序字段不合法");
}
if (SqlInjectionUtils.check(column)) {
log.warn("排序字段命中注入检测: {}", column);
throw new CheckedException("排序字段不合法");
}
return asc ? OrderItem.asc(column) : OrderItem.desc(column);
}
return page;
}
}
@@ -18,8 +18,10 @@
package com.bsd.pjcloud.common.sentinel.handle;
import com.alibaba.csp.sentinel.Tracer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.bsd.pjcloud.common.core.exception.CheckedException;
import com.bsd.pjcloud.common.core.exception.ServiceException;
import com.bsd.pjcloud.common.core.util.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
@@ -40,111 +42,145 @@ import java.util.List;
@RestControllerAdvice
public class GlobalBizExceptionHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 全局异常.
*
* @param e the e
* @return R
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleGlobalException(Exception e) {
/**
* 业务可控异常:允许把明确的业务文案返回前端。
*/
@ExceptionHandler({CheckedException.class, ServiceException.class, IllegalArgumentException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleBizException(RuntimeException e) {
log.warn("业务异常 ex={}", e.getMessage());
return R.failed(e.getMessage());
}
log.error("全局异常信息 ex={}", e.getMessage(), e);
/**
* SQLException:禁止把完整 SQL / 表结构回显给前端。
*/
@ExceptionHandler({SQLException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleSQLException(SQLException exception) {
log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
return R.failed("数据库调用异常,请联系管理员处理");
}
// 业务异常交由 sentinel 记录
Tracer.trace(e);
return R.failed(e.getLocalizedMessage());
}
/**
* 全局异常:默认不回显底层异常原文(MyBatis 常把 SQL、mapper 路径塞进 message)。
*
* @param e the e
* @return R
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleGlobalException(Exception e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
Tracer.trace(e);
if (isDataAccessRelated(e)) {
return R.failed("数据库调用异常,请联系管理员处理");
}
return R.failed("系统繁忙,请稍后重试");
}
/**
* SQLException Exception
*
* @param exception 数据库调用异常
* @return R
*/
@ExceptionHandler({SQLException.class})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleSQLException(SQLException exception) {
log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
return R.failed("数据库调用异常,请联系管理员处理");
}
/**
* 识别 Spring DAO / MyBatis / JDBC 相关异常(本模块不直接依赖 mybatis,用类名与特征串判断)。
*/
private static boolean isDataAccessRelated(Throwable e) {
Throwable t = e;
while (t != null) {
if (t instanceof SQLException) {
return true;
}
String name = t.getClass().getName();
if (name.startsWith("org.springframework.dao.")
|| name.startsWith("org.springframework.jdbc.")
|| name.contains("MyBatis")
|| name.contains("PersistenceException")
|| name.contains("BadSqlGrammar")) {
return true;
}
String msg = t.getMessage();
if (msg != null && (msg.contains("### Error") || msg.contains("### SQL")
|| msg.contains("SQL:") || msg.contains("Cause: java.sql")
|| msg.contains(" bad SQL grammar"))) {
return true;
}
t = t.getCause();
}
return false;
}
@ExceptionHandler(FeignException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleGlobalException(FeignException e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
@ExceptionHandler(FeignException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R handleGlobalException(FeignException e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
// 业务异常交由 sentinel 记录
Tracer.trace(e);
// 业务异常交由 sentinel 记录
Tracer.trace(e);
if (e.responseBody().isPresent()) {
try {
return objectMapper.readValue(e.responseBody().get().array(), R.class);
} catch (Exception parseEx) {
// 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端
log.warn("Feign 错误响应非 JSON,status={}", e.status());
return R.failed("下游服务异常,请稍后重试");
}
}
if (e.responseBody().isPresent()) {
try {
return objectMapper.readValue(e.responseBody().get().array(), R.class);
} catch (Exception parseEx) {
// 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端
log.warn("Feign 错误响应非 JSON,status={}", e.status());
return R.failed("下游服务异常,请稍后重试");
}
}
return R.failed("服务调用失败,请稍后重试");
}
return R.failed("服务调用失败,请稍后重试");
}
/**
* AccessDeniedException
*
* @param e the e
* @return R
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public R handleAccessDeniedException(AccessDeniedException e) {
log.error("拒绝授权异常信息 ex={}", e.getMessage());
return R.failed("权限不足,不允许访问");
}
/**
* AccessDeniedException
*
* @param e the e
* @return R
*/
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public R handleAccessDeniedException(AccessDeniedException e) {
log.error("拒绝授权异常信息 ex={}", e.getMessage());
return R.failed("权限不足,不允许访问");
}
/**
* validation Exception
*
* @param exception
* @return R
*/
@ExceptionHandler({BindException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleBodyValidException(BindException exception) {
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
// 插入log 的逻辑
return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
}
/**
* validation Exception
*
* @param exception
* @return R
*/
@ExceptionHandler({BindException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R handleBodyValidException(BindException exception) {
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
}
/**
* 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
*
* @return R
*/
@DeleteMapping("/error")
@ResponseStatus(HttpStatus.NOT_FOUND)
public R noHandlerFoundException() {
return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
}
/**
* 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
*
* @return R
*/
@DeleteMapping("/error")
@ResponseStatus(HttpStatus.NOT_FOUND)
public R noHandlerFoundException() {
return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
}
/**
* 保持和低版本请求路径不存在的行为一致
* <p>
* <a href="https://github.com/spring-projects/spring-boot/issues/38733">[Spring Boot
* 3.2.0] 404 Not Found behavior #38733</a>
*
* @param exception
* @return R
*/
@ExceptionHandler({NoResourceFoundException.class})
@ResponseStatus(HttpStatus.NOT_FOUND)
public R noResourceFoundException(NoResourceFoundException exception) {
log.debug("请求路径 404 {}", exception.getMessage());
return R.failed(exception.getMessage());
}
/**
* 保持和低版本请求路径不存在的行为一致
* <p>
* <a href="https://github.com/spring-projects/spring-boot/issues/38733">[Spring Boot
* 3.2.0] 404 Not Found behavior #38733</a>
*
* @param exception
* @return R
*/
@ExceptionHandler({NoResourceFoundException.class})
@ResponseStatus(HttpStatus.NOT_FOUND)
public R noResourceFoundException(NoResourceFoundException exception) {
log.debug("请求路径 404 {}", exception.getMessage());
return R.failed(exception.getMessage());
}
}