34-安全复盘:96个评审问题与修复
522行CODE_REVIEW_REPORT记录了96个问题------16个CRITICAL、26个HIGH、26个MEDIUM、28个LOW。最危险的是2.11组合攻击链:无鉴权+SQL拼接+rawSql=全库泄露。这篇复盘CRITICAL级的11个问题、P0-P3优先级体系、以及修复后的验证闭环。
文章目录
- 34-安全复盘:96个评审问题与修复
-
- 一、评审的全景
- 二、最危险攻击链:2.11的三段组合
- 三、其他CRITICAL的修复对照
-
- [2.1 JWT Secret硬编码(第25篇已讲修复)](#2.1 JWT Secret硬编码(第25篇已讲修复))
- [2.3/2.4 RowSet的两个脏标记CRITICAL(第19篇专述)](#2.3/2.4 RowSet的两个脏标记CRITICAL(第19篇专述))
- [2.5 事务竞态(第06篇已讲修复)](#2.5 事务竞态(第06篇已讲修复))
- [2.7 KingBasePg参数顺序(第09篇已讲)](#2.7 KingBasePg参数顺序(第09篇已讲))
- [2.9 前端XSS:innerHTML+script注入](#2.9 前端XSS:innerHTML+script注入)
- [2.10 路径遍历(第04篇提过)](#2.10 路径遍历(第04篇提过))
- 四、P0-P3优先级:修复的排序法
- 五、这次评审的方法论价值
- 六、遗留的LOW们要不要修
素材:
D:\work\browise\CODE_REVIEW_REPORT.md(522行)+ BROWISE-STATUS.md修复记录
一、评审的全景
| 维度 | 数据 |
|---|---|
| 范围 | 13个Maven模块+Vue前端 |
| Java源文件 | 214个 |
| Vue/TS源文件 | 91个 |
| 总问题 | 96个 |
| CRITICAL | 16个 |
| HIGH | 26个 |
96个问题不是写得烂------是认真查了 。667个单测100%通过的系统照样有16个CRITICAL------测试通过≠安全。单测验证"功能对不对",评审发现"能不能被恶意用"。
二、最危险攻击链:2.11的三段组合
第一段: EaAdminController无鉴权 (formbuilder)
↓ 任意登录用户可POST /ea/admin/save
第二段: SqlBuilder固定值直接拼接 (metadata)
↓ 恶意SQL片段写入EA05条件元数据
第三段: /ea/query执行rawSql (formbuilder)
↓ 拼接的SQL随查询执行
结果:数据库全量泄露与篡改。
单独看每一段都不致命------admin接口没鉴权("内部系统先跑起来")、固定值拼接("值来自配置表是可信的")、rawSql执行("#{param}是参数化的")。三段串起来是完整绕过。这就是评审必须跨模块看的原因------单模块review看不到组合面。
修复三件套(已全部落地):
java
// ①EaAdminController每个方法加checkAuth
private void checkAuth() {
UserProfile p = CurrentUser.get();
if (p == null || !adminList.contains(p.getPsnId()))
throw new BrowiseException(403, "无元数据管理权限");
}
// ②改删接口的固定值也走占位符(第07篇appendWhereConditions)
sql.append(c.getColumnName()).append("=?"); // 原来是='值'直接拼
paramValues.add(c.getConditionValue());
// ③rawSql强制#{}参数化(第11篇)------占位符替换从根上堵注入
三、其他CRITICAL的修复对照
2.1 JWT Secret硬编码(第25篇已讲修复)
java
// 修复前
private String secret = "browise-platform-jwt-secret-key-2026"; // 源码里的死密钥
// 修复后:@PostConstruct启动校验------无配置或短于32字符直接起不来
2.3/2.4 RowSet的两个脏标记CRITICAL(第19篇专述)
- reject()用clear()清数据而不是_o恢复 → 修复为clear+putAll(originals)整体替换
- accept()不清originals → 修复为复位全部状态承载
有意思的对照 ------评审报告指出"TypedRowSet.reject()实现正确"------同一个协议的两个载体,一个对一个错。没有共享实现的协议=语义漂移的温床。修复后两边行为对齐,但代码依然是两份(泛型/Map载体差异所致)------评审建议的"抽公共trait"在Java 8里没有优雅解法,靠测试锁定行为等价(BaseEntityTest 24个用例)。
2.5 事务竞态(第06篇已讲修复)
java
// 修复前:catch里rollback + finally里无条件commit------异常后先滚再提交!
} catch (EaException e) {
if (myTrans) provider.rollback();
throw e;
} finally {
if (myTrans) provider.commit(); // ← rollback之后又commit了空事务
}
// 修复后:success-flag------只有成功路径才commit
boolean success = false;
try { ...; success = true; return; }
finally {
if (myTrans) { if (success) commit(); else rollback(); }
}
2.7 KingBasePg参数顺序(第09篇已讲)
LIMIT ? OFFSET ?的参数序是size,offset------引擎按ROWNUM习惯追加min,max------supportsOffset()布尔值就是这次修复的产物。
2.9 前端XSS:innerHTML+script注入
ApproveForm把审批表单HTML直接innerHTML渲染、script用createElement执行------恶意流程表单=存储型XSS=偷所有查看者的JWT。修复引入DOMPurify过滤。
2.10 路径遍历(第04篇提过)
java
// 修复前
new File(uploadPath, fileName); // fileName="../../../etc/passwd"直接拼
// 修复后:normalize+startsWith白名单
Path resolved = Paths.get(uploadPath).resolve(fileName).normalize();
if (!resolved.startsWith(Paths.get(uploadPath))) throw new SecurityException();
四、P0-P3优先级:修复的排序法
P0(立即修)--- 11个:全部CRITICAL安全+数据损坏
JWT硬编码/密码明文/SQL注入/XSS/路径遍历/RowSet损坏/事务竞态...
P1(本迭代)--- HIGH里的正确性问题
连接泄漏/分页参数/BaseEntity脏标记误报/前端绑定失效
P2(下迭代)--- MEDIUM性能与健壮性
反射缓存/空catch/日志脱敏
P3(择机) --- LOW规范类
命名/Javadoc/魔法数字
P0的11个已全部修复 (BROWISE-STATUS的P0-2.x系列)------修复记录每条带问题编号对应回评审报告,形成"发现→定级→修复→记录"的完整闭环。
五、这次评审的方法论价值
三层视角缺一不可:
| 视角 | 能发现 | 单测能不能发现 |
|---|---|---|
| 功能正确性 | reject恢复错值 | 能(但没写这个用例) |
| 安全攻击面 | 无鉴权+注入组合 | 不能------单测不模拟攻击者 |
| 跨模块组合 | 三段攻击链 | 不能------单测按模块隔离 |
"667个测试全通过"和"16个CRITICAL"同时成立 ------这个事实本身就是最深的教训:测试覆盖的是你想到的路径,攻击者走的是你没想到的路径。
六、遗留的LOW们要不要修
28个LOW(空catch、命名、注释)------择机修 。不是不重要,是机会成本:修28个LOW的时间够写3个新功能。只要LOW不升级(空catch在关键路径出现就该提级),技术债的利息可控。
评审报告的价值不是"96个都要修"------是让每个问题有编号、有定级、有去向(修了/择机/不修的理由)。看不见的债才可怕。
✅ 亮点:2.11三段组合攻击链的完整剖析(单段无害组合致命)、修复与前期篇章的交叉引用闭环、success-flag修复对照、P0-P3的排序逻辑、"667测试通过与16个CRITICAL并存"的方法论教训。适合建立代码评审体系的人。扩展方向:第57篇667单测、第58篇18个bug修复史、第31篇TrustAll教训。