文章目录
SmartOA 要过三级等保,安全审计拆出 P0~P3 四级问题。P0 是必须立刻修的:SQL 注入、路径遍历、IP 伪造、默认密码、明文 token。这一批修复横跨后端(拦截器/加密/权限)与前端(加密方案),每一条都有对应的攻击场景。这篇记录 P0 批次的完整加固过程。
一、P0 问题清单与攻击场景
| 问题 | 攻击场景 | 风险 |
|---|---|---|
| SQL 注入 | 拼接查询条件注入 OR 1=1 | 数据泄露/删库 |
| 路径遍历 | ../../etc/passwd 读任意文件 | 服务器沦陷 |
| IP 伪造 | X-Forwarded-For 伪造来源 IP 绕过限流 | 刷接口/绕封禁 |
| 默认密码 | admin/admin123 未改直接登录 | 账户接管 |
| 明文 token | URL/Cookie 明文携带 token 被日志/中间人截获 | 会话劫持 |
二、SQL 注入:白名单校验 + 参数化
审计发现部分查询接口存在拼接 SQL 的风险。修复分两层:
- 参数化查询:MyBatis-Plus 的 QueryWrapper 全部走参数绑定,杜绝字符串拼接
- 排序字段白名单:orderBy 这类「动态列名」不能参数化,必须白名单校验——前端传的排序字段名不在白名单里就拒绝
// 排序字段白名单:动态列名无法参数化,只能白名单
private static final Set<String> SORT_WHITELIST =
Set.of("create_time", "update_time", "status", "amount");
public void applySort(QueryWrapper<?> qw, String column, String dir) {
if (!SORT_WHITELIST.contains(column)) {
throw new IllegalArgumentException("非法排序字段: " + column);
}
qw.orderBy(true, "asc".equalsIgnoreCase(dir), column);
}
三、路径遍历:文件下载统一校验
文件下载/预览接口如果直接拼接用户传入的文件名,就能 ../../ 穿越目录。修复方案:
// 文件名规范化 + 目录约束
Path base = Paths.get(uploadRoot).toAbsolutePath().normalize();
Path target = base.resolve(fileName).normalize();
// 关键:解析后必须仍在 base 目录内,否则拒绝
if (!target.startsWith(base)) {
throw new AccessDeniedException("非法文件路径");
}
核心就是 normalize() 后再 startsWith(base) 校验——先规范化(去掉 .. ),再判断是否逃出根目录。
四、IP 伪造:统一取真实客户端 IP
限流/封禁都依赖客户端 IP,但如果直接读 X-Forwarded-For 头,攻击者可以自己伪造。审计发现项目里有 16 行手写的 XFF 解析逻辑,且与框架的 JakartaServletUtil.getClientIP() 不一致。修复:
- 统一使用
JakartaServletUtil.getClientIP()(框架已处理代理链信任逻辑) - 删除手写解析(RateLimiterAspect 等 3 处)
- nginx 层配置
X-Real-IP传递,只信任来自反代层的第一跳 IP
五、默认密码与密码策略
默认管理员密码是历史遗留的安全隐患,修复:
- 部署 SQL 加安全提醒:上线后立即修改默认账号密码
- 密码策略:长度 + 复杂度校验(大小写/数字/符号),Argon2 哈希存储
- 密码历史:
SecurityPasswordService用 Argon2 存历史密码,防止改回旧密码(C4 修复——原先明文 vs 哈希比较永远 false,复用检查实际是失效的) - 登录失败锁定:连续失败锁定账户(配合 IP 速率限制 + 自动封禁)
六、明文 token:Cookie httpOnly + 密钥交换
这是最大的一块。原来 token 存在 localStorage,且部分场景出现在 URL 里;前端加密密钥硬编码在 JS 里。三层修复:
1. token 迁移到 httpOnly Cookie
localStorage 的 token 能被任何 XSS 脚本读走。改为 httpOnly Cookie 后 JS 读不到,XSS 拿不到会话:
// Sa-Token 启用 Cookie 读写 + HttpOnly + SameSite
sa-token:
token-name: Authorization
is-read-cookie: true // 从 Cookie 读 token
is-read-header: true // 同时兼容 Header(API 调用方)
cookie:
http-only: true // JS 不可读
same-site: lax // 防 CSRF
迁移坑:Cookie 值带 Bearer 前缀,而 Sa-Token 读 Cookie 时不自动 strip——前端登录后写 Cookie 要去掉前缀,否则每次校验都失败(commit: fix Cookie值去掉Bearer前缀)。
2. 前端加密密钥:RSA 密钥交换替代硬编码
原来前端加密密钥(AES/SM4)写死在 JS 里,等于加密白做。改为方案 A:前端随机生成 AES 会话密钥,用后端 RSA 公钥加密传输,后端解密后用于本次会话(AES 密钥每次会话随机,兼容旧 v1 格式向后兼容):
// 前端:随机 AES 密钥 + RSA 加密传输
const aesKey = crypto.getRandomValues(new Uint8Array(16)); // 随机 128-bit
const encryptedKey = JSEncrypt('-----BEGIN PUBLIC KEY-----...')
.encrypt(aesKey);
// 后端:RSA 私钥解密出 AES 密钥,再解业务数据
byte[] aesKeyBytes = rsaDecrypt(encryptedKey); // PKCS1Padding 对齐前端
联调坑:前端 JSEncrypt 默认 PKCS1Padding,后端 Java 若用 OAEP 会解密失败——两边 padding 必须对齐(commit: KeyExchangeService RSA padding OAEP→PKCS1Padding)。
3. 钉钉登录 token:code 交换替代明文传递
钉钉登录回调 URL 里直接带 token(可被日志/历史记录截获),改为临时 code + 后端交换:回调只传一次性 code,后端用 code 换真实 token;临时 token 加 Redis 一次性消费(GETDEL 原子操作防重放,A1 修复)。
七、顺带:XSS 与 CVE 补丁
- 前端 8 处
v-html全部加 DOMPurify sanitize(help-doc/notice/change-log/data-tracer 等) - 后端 notice-detail / change-log-detail 加 sanitizeHtml 工具处理
- crypto-js 4.1.1 → 4.2.0 修复 CVE-2023-46233 原型污染
- Cookie 配置 HttpOnly + SameSite(TokenConfig 代码级设置)
八、P0 批次修复清单(可直接复用)
- SQL 注入:参数化 + 动态列名白名单,写注入测试
- 路径遍历:normalize() + startsWith(base) 双重校验
- IP 伪造:统一框架取 IP 工具,删手写解析,nginx 只信第一跳
- 默认密码:部署提醒 + 密码策略 + Argon2 历史密码(哈希比较要验证真的生效)
- token:httpOnly Cookie + 前端随机密钥 RSA 交换 + 钉钉 code 交换防重放
- XSS:v-html 全量 DOMPurify,安全相关库及时升 CVE 补丁
评 论