Java 安全扫描中高危等级缺陷修复指南
Problem
Java / Spring Boot 项目在安全代码扫描后,通常会产生数百到数千个中高等级缺陷,涵盖信息泄露、HTTP响应截断、流泄漏、路径遍历、不安全的框架绑定、SQL 注入、XSS 等类别。需要系统性地分类、制定计划、实施修复并验证编译。
Context / Trigger Conditions
编码/审查时(预防与修复均可触发):
- 编写或修改
GlobalExceptionHandler等全局异常处理器 - 设置 HTTP 响应头(
Content-Disposition、Content-Type、Set-Cookie等)且值来自用户输入 - 拼接用户传入的文件路径进行文件读写、移动、复制、删除操作
- 使用
FileInputStream、FileOutputStream、XSSFWorkbook、PdfWriter、ServletOutputStream等流对象 - Spring MVC Controller 方法直接使用 Bean/VO 接收请求参数(
public String add(User user)) - 处理 ZIP 解压、文件上传 MultipartFile、Base64 转文件等场景
- 对密码/密钥/随机数进行编码或加密操作
- 编写或修改 MyBatis Mapper XML / @Select / @Update,使用
${}拼接参数 - 将用户输入写入 Excel、HTML、PDF 或前端页面(存在 XSS 风险)
- Controller 方法直接将用户输入回显到响应或视图(反射型 XSS)
- 技术栈:Spring Boot + Spring MVC + MyBatis + Apache POI
收到扫描报告时:
- 代码扫描报告(常见格式:Word .docx、PDF、HTML)
- 扫描工具:fxnk、Fortify、Checkmarx、SonarQube 等
- 中危缺陷数量通常在 1000+,需要分阶段实施
Solution
0. 预防性安全编码规范(写代码时直接遵循)
在生成或修改代码时,直接应用以下规范以避免中危缺陷:
| 场景 | 安全规范 |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| 全局异常处理 | 通用 Exception 捕获后返回固定模糊提示(如 "系统繁忙,请稍后重试"),禁止返回 e.getMessage() 给前端 |
| HTTP 响应头 | 用户传入的字符串写入响应头前,必须过滤 \r\n:value.replaceAll("[\\r\\n]", "") |
| 文件流操作 | 所有 FileInputStream / FileOutputStream / Workbook / PdfWriter 必须使用 try-with-resources |
| 文件路径拼接 | 用户传入的路径使用 getCanonicalPath() 校验,确保结果路径在允许的根目录范围内 |
| Spring MVC 参数绑定 | 公共基类的 @InitBinder 中禁用敏感字段:binder.setDisallowedFields(...) |
| 文件上传 | 校验 MIME Type、扩展名白名单、文件头 Magic Number,上传后重命名为随机文件名 |
| SQL 查询 | MyBatis 中禁止 ${} 拼接用户输入,统一使用 #{} 参数化查询;动态表名列名需先做白名单校验 |
| XSS 防护 | 输出到 Excel/HTML/页面的用户数据先做 HTML 转义;前端输入做白名单校验,禁止直接回显未过滤的用户输入 |
1. 读取 .docx 扫描报告
若报告为 .docx 二进制文件,用 ZIP + XML 提取文本:
$temp = [IO.Path]::GetTempPath() + [Guid]::NewGuid().ToString()
[System.IO.Compression.ZipFile]::ExtractToDirectory('report.docx', $temp)
$xml = [xml](Get-Content "$temp\word\document.xml" -Encoding UTF8 -Raw)
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main')
$text = ($xml.SelectNodes('//w:t', $ns) | ForEach-Object { $_.InnerText }) -join ''
2. 缺陷分类与优先级排序
按风险影响 + 修复难度排序:
| 优先级 | 缺陷类别 | 典型数量 | 修复难度 | | --- | ----------------------------------- | ----- | -------- | | P0 | SQL 注入 + 存储型 XSS + 反射型 XSS + 高危路径遍历 | ~500+ | 中 | | P1 | 信息泄露 + HTTP响应截断 | ~10 | 低 | | P2 | 流资源未释放 + 路径遍历 | ~70 | 中 | | P3 | 框架绑定 / Mass Assignment | ~300+ | 中 | | P4 | 数据库访问控制 | ~900+ | 高(需评估误报) | | P5 | 密码管理 / 加密算法 | ~30 | 高(需兼容评估) |
3. 常见缺陷修复代码模式
3.1 信息泄露(全局异常处理器)
问题:handleException(Exception.class) 返回 e.getMessage(),可能暴露堆栈/SQL。
修复:返回统一模糊提示,详细异常只记录日志。
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e) {
log.error(e.getMessage(), e);
return AjaxResult.error("系统繁忙,请稍后重试");
}
3.2 HTTP 响应截断(Content-Disposition)
问题:用户可控文件名直接拼接到响应头,未过滤 \r\n。
修复:在编码前过滤换行符。
fileName = fileName.replaceAll("[\\r\\n]", "");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
3.3 流资源未释放
问题:FileInputStream / FileOutputStream / Workbook 等未在 finally 中关闭。
修复:统一使用 try-with-resources。
// 修复前
FileInputStream fis = new FileInputStream(file);
XSSFWorkbook wb = new XSSFWorkbook(fis);
// ... 业务逻辑 ...
// 缺少关闭
// 修复后
try (FileInputStream fis = new FileInputStream(file);
XSSFWorkbook wb = new XSSFWorkbook(fis)) {
// ... 业务逻辑 ...
}
关键类需检查:FileUtils、ExcelUtils、PdfUtils、HtmlUtils、CsvUtils。
3.4 路径遍历
问题:用户传入的路径直接拼接根目录,可能导致 ../../etc/passwd。
修复:使用 getCanonicalPath() 校验结果路径必须在根目录内。
private void validatePath(String path) {
try {
File file = new File(rootPath, path);
String canonicalPath = file.getCanonicalPath();
String canonicalRoot = new File(rootPath).getCanonicalPath();
if (!canonicalPath.startsWith(canonicalRoot)) {
throw new BaseException("非法文件路径");
}
} catch (IOException e) {
throw new BaseException("文件路径校验失败");
}
}
3.5 Spring MVC 不安全的框架绑定(Mass Assignment)
问题:Controller 直接绑定请求参数到 Bean,攻击者可批量赋值敏感字段(如 admin=true)。
修复:在公共基类的 @InitBinder 中全局禁用敏感字段。
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setDisallowedFields("admin", "role", "password", "createTime",
"updateTime", "createBy", "updateBy", "deleted", "delFlag", "tenantId");
// ... 其他类型转换注册
}
3.6 SQL 注入(MyBatis 动态 SQL)
问题:MyBatis Mapper 中使用 ${} 拼接用户输入,攻击者可注入任意 SQL。
修复:将 ${} 替换为 #{} 参数化查询;动态表名/列名必须走白名单校验。
// 修复前(危险)
@Select("SELECT * FROM ${tableName} WHERE id = ${id}")
User selectById(String tableName, Long id);
// 修复后
@Select("SELECT * FROM #{tableName} WHERE id = #{id}") // 错误:#{} 不能用于表名
// 正确做法:表名走白名单,值用 # {}
private static final Set<String> ALLOWED_TABLES = Set.of("sys_user", "sys_dept");
public User selectById(String tableName, Long id) {
if (!ALLOWED_TABLES.contains(tableName)) {
throw new BaseException("非法表名");
}
return sqlSession.selectOne("SELECT * FROM " + tableName + " WHERE id = #{id}", id);
}
3.7 存储型 XSS(导出/模板渲染场景)
问题:将用户输入的数据直接写入 Excel、HTML、PDF,前端再次展示时触发脚本执行。 修复:在写入前对用户数据进行 HTML 转义,或设置 Excel 单元格为纯文本类型。
// 修复前
row.createCell(0).setCellValue(user.getRemark()); // 用户输入可能含 <script>
// 修复后
String safeRemark = HtmlUtils.htmlEscape(user.getRemark());
row.createCell(0).setCellValue(safeRemark);
// 或者设置单元格类型为 STRING,避免公式执行
Cell cell = row.createCell(0);
cell.setCellValue(user.getRemark());
cell.setCellType(CellType.STRING);
3.8 反射型 XSS(参数回显场景)
问题:Controller 直接将用户输入回显到页面/响应,未做过滤或转义。 修复:对回显参数做输入校验 + 输出编码,Spring Boot 可开启默认 HTML 转义。
// 修复前
@GetMapping("/search")
public String search(String keyword, Model model) {
model.addAttribute("keyword", keyword); // 直接回显
return "searchResult";
}
// 修复后
@GetMapping("/search")
public String search(@RequestParam String keyword, Model model) {
// 白名单校验:仅允许中文、英文、数字、空格
if (!keyword.matches("^[\\u4e00-\\u9fa5a-zA-Z0-9\\s]+$")) {
throw new BaseException("搜索关键字包含非法字符");
}
model.addAttribute("keyword", keyword);
return "searchResult";
}
3.9 高危路径遍历(文件名/存储路径校验)
问题:用户传入的 storeName、fileName 等直接作为文件路径或文件名,未校验 ../ 或绝对路径。
修复:对文件名提取后校验,禁止路径分隔符;完整路径使用 getCanonicalPath() 校验。
private void validateFileName(String fileName) {
if (fileName == null || fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
throw new BaseException("非法文件名");
}
}
private void validatePath(String rootPath, String path) {
try {
File file = new File(rootPath, path);
String canonicalPath = file.getCanonicalPath();
String canonicalRoot = new File(rootPath).getCanonicalPath();
if (!canonicalPath.startsWith(canonicalRoot)) {
throw new BaseException("非法文件路径");
}
} catch (IOException e) {
throw new BaseException("文件路径校验失败");
}
}
4. 验证方式
修复后必须执行:
mvn clean compile -DskipTests
重点关注:文件上传/下载、Excel 导出、PDF 生成等核心流程的回归测试。
Verification
mvn clean compile -DskipTests编译通过,无新增报错- 回归测试:登录、文件上传下载、Excel 导出、异常触发后的前端提示正常
- 复扫:使用相同检测模板重新扫描,目标中危缺陷数量下降
Example
场景:扫描报告提示 ExcelUtils.setResponseHeader 存在 HTTP 响应截断。
// 修复前
fileName = new String(fileName.getBytes("utf-8"), "ISO-8859-1");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 修复后
fileName = fileName.replaceAll("[\\r\\n]", "");
fileName = new String(fileName.getBytes("utf-8"), "ISO-8859-1");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
Notes
- 密码管理/加密算法类缺陷(DES 转 AES、MD5 升级等)涉及存量数据兼容性,需单独评估迁移方案(双算法支持 + 登录时自动迁移)。
- 数据库访问控制类缺陷数量通常最多(900+),若系统已在 Controller/Service 层通过登录会话做了统一鉴权,多数为误报,优先修复暴露在外网或无 Token 的接口。
- 扫描报告中的行号可能与当前代码分支不完全一致,需结合跟踪路径(调用链)定位实际爆发行。
References
- OWASP Top 10 2021: https://owasp.org/Top10/
- CWE-22 (Path Traversal): https://cwe.mitre.org/data/definitions/22.html
- CWE-79 (Cross-site Scripting): https://cwe.mitre.org/data/definitions/79.html
- CWE-89 (SQL Injection): https://cwe.mitre.org/data/definitions/89.html
- CWE-116 (HTTP Response Splitting): https://cwe.mitre.org/data/definitions/116.html
- CWE-20 (Improper Input Validation): https://cwe.mitre.org/data/definitions/20.html
扫码联系在线客服