feat(B3): custom_css 站点级自定义样式-服务端黑名单强校验+5主题head内联直出

- validateConfigJson 增 custom_css 分支:≤10240字符;黑名单忽略大小写拒 </style/<script/@import/expression(
- 快照/渲染链路(model customCss)与 themeVarsCss 同构注入;缓存失效复用既有 SiteContentChangedEvent
- 5 套主题 head.html 于 main.css 与微调变量之后 th:utext 直出(安全性完全依赖服务端先行)
- SiteServiceIT+6 例(含 expression 红线项与 10240 边界);PortalSsrIT+3 例(渲染/head内/顺序/缓存失效)
- mvn -o test 258 例全绿(B3 状态,不含 B4 红测)
This commit is contained in:
chenxing
2026-09-07 20:14:23 +08:00
parent b4822a7f38
commit 19e414ee81
11 changed files with 190 additions and 0 deletions
@@ -59,6 +59,12 @@ public class SiteService {
java.util.Collections.unmodifiableSet(new java.util.HashSet<>(java.util.Arrays.asList("theme_params")));
private static final int CONFIG_VALUE_MAX_LEN = 500;
/** custom_css 上限与黑名单(B3):服务端强校验,前端仅提示;黑名单为防御下限(方案风险表) */
private static final int CUSTOM_CSS_MAX_LEN = 10240;
private static final List<String> CUSTOM_CSS_BLACKLIST =
java.util.Collections.unmodifiableList(java.util.Arrays.asList(
"</style", "<script", "@import", "expression("));
/** 当前用户可管理的站点列表(含上级站点名回填,展示"主站/院系子站"层级) */
public List<SiteVo> listFor(BoaUser user) {
List<CmsSite> sites = authProperties.isSuperAdmin(user)
@@ -290,6 +296,11 @@ public class SiteService {
if (!StringUtils.hasText(text)) {
return;
}
// custom_css 独立校验(B3):上限 10KB + 黑名单,不走通用 500 字上限
if ("custom_css".equals(key)) {
validateCustomCss(text);
return;
}
if (text.length() > CONFIG_VALUE_MAX_LEN) {
throw new BizException(ErrorCode.PARAM_ERROR, "站点配置值过长:" + key + "(上限 500 字)");
}
@@ -318,4 +329,23 @@ public class SiteService {
}
});
}
/**
* custom_css 校验(B3,服务端强校验):长度 ≤10240;黑名单忽略大小写拒绝
* {@code </style}(逃逸内联 style 块)、{@code <script}、{@code @import}(外链样式注入)、
* {@code expression(}(IE 表达式)。门户 5 套主题 head 以 th:utext 直出,安全性完全依赖本方法先行。
*/
private void validateCustomCss(String css) {
if (css.length() > CUSTOM_CSS_MAX_LEN) {
throw new BizException(ErrorCode.PARAM_ERROR,
"custom_css 过长(" + css.length() + " 字符,上限 " + CUSTOM_CSS_MAX_LEN + ")");
}
String lower = css.toLowerCase(java.util.Locale.ROOT);
for (String banned : CUSTOM_CSS_BLACKLIST) {
if (lower.contains(banned)) {
throw new BizException(ErrorCode.PARAM_ERROR,
"custom_css 含禁用内容:" + banned + "(不允许 </style、<script、@import、expression())");
}
}
}
}