
在数字化业务高速发展的今天,Java作为企业级应用的核心开发语言,承载了绝大多数核心业务系统的运行。但多数开发团队往往只关注功能的实现与性能的优化,却忽略了代码层面的安全隐患——小到敏感信息泄露,大到服务器被入侵、核心数据被窃取,绝大多数安全事件的根源,都来自于代码中可被利用的安全漏洞。
代码安全审计,是通过系统化的方法对源代码进行全面扫描与深度分析,提前发现并修复安全隐患的核心手段,也是构建应用安全防护体系的第一道防线。
安全审计的前提,是建立统一的安全编码规范。规范是安全的底线,也是审计过程中判断代码是否存在风险的核心依据。本章节将从6个核心维度,建立覆盖全开发流程的安全编码规范体系。
所有安全漏洞的根源,几乎都来自于“不可信的外部输入”。输入输出是应用与外界交互的唯一通道,也是安全防护的第一道关卡。
数据是业务的核心资产,也是黑客攻击的核心目标。数据安全规范覆盖数据的全生命周期,是审计过程中的重点核查项。
权限控制是防止未授权访问的核心机制,也是业务系统中最容易出现逻辑漏洞的环节。
异常处理与日志,是排查问题的核心工具,也是最容易泄露敏感信息的环节。
超过60%的Java应用安全漏洞,来自于第三方依赖组件。依赖管理是安全审计中不可忽视的环节。
Java的序列化机制是高危漏洞的重灾区,反序列化漏洞也是Java应用中最常见的远程代码执行漏洞来源。
本章节将覆盖Java应用中最常见、危害最高的核心安全漏洞,拆解每类漏洞的底层触发逻辑,梳理标准化的排查方法,配合错误与正确的代码实例,帮助开发者掌握精准的漏洞排查与修复能力。
项目基础依赖配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>security-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security-demo</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.1.0-jre</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
SQL注入是最经典、危害最高的代码漏洞,黑客可通过该漏洞直接操作数据库,导致数据泄露、数据篡改、数据库被破坏,甚至获取服务器权限。
SQL注入的核心原理,是将用户可控的外部输入,未经处理直接拼接到SQL语句中,导致数据库将用户输入的恶意内容,解析为SQL指令执行。开发者预期用户输入的是“查询条件”,但用户输入的是“SQL命令”,而代码直接将这段命令拼接到了SQL语句中,数据库无法区分这是开发者写的代码还是用户输入的内容,最终执行了恶意指令。
错误示例,MyBatis-Plus的SQL注入错误写法:
package com.jam.demo.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 用户控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户相关接口")
publicclass UserController {
privatefinal UserService userService;
/**
* 错误示例:SQL注入漏洞
* 直接将用户输入的参数拼接到SQL条件中,使用last方法拼接SQL
*/
@GetMapping("/list/error")
@Operation(summary = "用户列表查询(错误示例)", description = "存在SQL注入漏洞的查询接口")
public List<User> listUserError(@RequestParam String username) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.last("WHERE username = '" + username + "'");
return userService.list(queryWrapper);
}
}
该示例中,用户如果输入' OR '1'='1,拼接后的SQL会查询出所有用户的数据;如果输入'; DROP TABLE user; --,会直接删除用户表。
MyBatis XML错误示例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.UserMapper">
<!-- 错误示例:使用${}拼接用户输入,导致SQL注入 -->
<select id="selectUserByUsername" resultType="com.jam.demo.entity.User">
SELECT * FROM user WHERE username = ${username}
</select>
</mapper>
${}会直接将参数内容拼接到SQL中,不会做任何转义,必然导致SQL注入。
正确修复示例:
package com.jam.demo.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 用户控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户相关接口")
publicclass UserController {
privatefinal UserService userService;
/**
* 正确示例:安全的用户列表查询
* 使用预编译机制,配合参数白名单校验
* @param username 用户名
* @return 用户列表
*/
@GetMapping("/list/safe")
@Operation(summary = "用户列表查询(安全示例)", description = "修复SQL注入漏洞的安全查询接口")
public List<User> listUserSafe(@RequestParam String username) {
if (!StringUtils.hasText(username) || !username.matches("^[a-zA-Z0-9_]{1,20}$")) {
return List.of();
}
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername, username);
return userService.list(queryWrapper);
}
}
MyBatis XML正确示例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.mapper.UserMapper">
<!-- 正确示例:使用#{}预编译占位符,自动处理参数转义,杜绝SQL注入 -->
<select id="selectUserByUsername" resultType="com.jam.demo.entity.User">
SELECT * FROM user WHERE username = #{username}
</select>
</mapper>
核心易混淆点区分:#{}和{}的本质区别。#{}是预编译占位符,MyBatis会将其替换为?,参数通过PreparedStatement的set方法传入,数据库会将参数作为值处理,不会解析为SQL指令;而{}是字符串替换,直接将参数内容拼接到SQL语句中,数据库会将其作为SQL的一部分解析,必然存在SQL注入风险。
必须使用动态拼接的特殊场景(如动态排序字段),必须通过白名单严格限制输入范围:
/**
* 安全的动态排序示例
* @param sortField 排序字段
* @return 用户列表
*/
@GetMapping("/list/sort")
@Operation(summary = "用户列表排序查询", description = "安全的动态排序接口")
public List<User> listUserBySort(@RequestParam String sortField) {
List<String> allowSortFields = Lists.newArrayList("create_time", "update_time", "username");
if (!allowSortFields.contains(sortField)) {
sortField = "create_time";
}
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.last("ORDER BY " + sortField + " DESC");
return userService.list(queryWrapper);
}
XSS漏洞是Web应用中最常见的前端安全漏洞,黑客可通过该漏洞在用户浏览器中执行恶意脚本,窃取用户Cookie、Session等认证信息,冒充用户执行操作。
XSS漏洞的核心原理,是应用将用户可控的输入内容,未经转义处理直接输出到HTML页面中,导致浏览器将用户输入的恶意内容,解析为JavaScript脚本执行。XSS漏洞分为三类:反射型XSS、存储型XSS、DOM型XSS。
错误示例,反射型XSS漏洞:
package com.jam.demo.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* XSS漏洞示例控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/xss")
@RequiredArgsConstructor
@Tag(name = "XSS示例", description = "XSS漏洞示例接口")
publicclass XssDemoController {
/**
* 错误示例:反射型XSS漏洞
* 直接将用户输入的内容返回,未做任何转义处理
*/
@GetMapping("/search/error")
@Operation(summary = "搜索接口(错误示例)", description = "存在反射型XSS漏洞的搜索接口")
public String searchError(@RequestParam String keyword) {
return"您搜索的关键词是:" + keyword;
}
}
当用户输入<script>alert(document.cookie)</script>时,页面会直接执行这段脚本,弹出用户的Cookie信息,导致认证信息被窃取。
正确修复示例:
package com.jam.demo.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.HtmlUtils;
/**
* XSS安全示例控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/xss")
@RequiredArgsConstructor
@Tag(name = "XSS示例", description = "XSS漏洞示例接口")
publicclass XssDemoController {
/**
* 正确示例:修复反射型XSS漏洞
* 对输出内容执行HTML转义,配合输入白名单校验
* @param keyword 搜索关键词
* @return 搜索结果
*/
@GetMapping("/search/safe")
@Operation(summary = "搜索接口(安全示例)", description = "修复XSS漏洞的安全搜索接口")
public String searchSafe(@RequestParam String keyword) {
if (!StringUtils.hasText(keyword) || keyword.length() > 50) {
return"您搜索的关键词无效";
}
String safeKeyword = HtmlUtils.htmlEscape(keyword);
return"您搜索的关键词是:" + safeKeyword;
}
}
经过转义后,用户输入的<script>标签会被转换为<script>,浏览器会将其作为普通文本渲染,不会执行脚本。
Java反序列化漏洞是危害最高的远程代码执行漏洞之一,黑客可通过该漏洞直接在服务器上执行任意命令,完全控制服务器。
Java的序列化机制,是将对象转换为字节流用于存储或传输;反序列化则是将字节流还原为对象。当应用反序列化来自不可信来源的字节流时,黑客可以构造恶意的序列化字节流,在反序列化的过程中,触发恶意类的readObject方法,执行任意代码。
错误示例,Java原生反序列化漏洞:
package com.jam.demo.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import java.io.ObjectInputStream;
/**
* 反序列化漏洞示例控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/deserialize")
@RequiredArgsConstructor
@Tag(name = "反序列化示例", description = "反序列化漏洞示例接口")
publicclass DeserializeDemoController {
/**
* 错误示例:Java原生反序列化漏洞
* 直接反序列化用户上传的字节流,无任何校验
*/
@PostMapping("/error")
@Operation(summary = "反序列化接口(错误示例)", description = "存在反序列化漏洞的接口")
public String deserializeError(HttpServletRequest request) {
try (ObjectInputStream ois = new ObjectInputStream(request.getInputStream())) {
Object obj = ois.readObject();
return"反序列化成功,对象类型:" + obj.getClass().getName();
} catch (Exception e) {
log.error("反序列化失败", e);
return"反序列化失败";
}
}
}
这个接口直接从请求体中读取字节流并反序列化,黑客可构造恶意序列化字节流,通过该接口在服务器上执行任意命令。
正确修复方案,首先配置fastjson2全局安全规则,关闭AutoType功能:
package com.jam.demo.config;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
import com.alibaba.fastjson2.support.spring.http.converter.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* Fastjson2配置类
* @author ken
*/
@Configuration
publicclass Fastjson2Config implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setReaderFeatures(JSONReader.Feature.SafeMode);
config.setCharset(StandardCharsets.UTF_8);
converter.setFastJsonConfig(config);
converter.setSupportedMediaTypes(List.of(MediaType.APPLICATION_JSON));
converters.add(0, converter);
}
}
安全的反序列化示例,指定明确的目标类型,禁用未知类型反序列化:
package com.jam.demo.controller;
import com.alibaba.fastjson2.JSON;
import com.jam.demo.entity.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 反序列化安全示例控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/deserialize")
@RequiredArgsConstructor
@Tag(name = "反序列化示例", description = "反序列化漏洞示例接口")
publicclass DeserializeDemoController {
/**
* 正确示例:安全的JSON反序列化
* 指定明确的反序列化类型,关闭AutoType,配合参数校验
* @param json 待反序列化的JSON字符串
* @return 反序列化结果
*/
@PostMapping("/safe")
@Operation(summary = "反序列化接口(安全示例)", description = "修复反序列化漏洞的安全接口")
public String deserializeSafe(@RequestBody String json) {
if (!StringUtils.hasText(json)) {
return"参数不能为空";
}
try {
User user = JSON.parseObject(json, User.class);
return"反序列化成功,用户名:" + user.getUsername();
} catch (Exception e) {
log.error("反序列化失败", e);
return"反序列化失败,参数格式错误";
}
}
}
核心安全原则:绝对禁止反序列化来自不可信来源的数据流;禁止使用Java原生的ObjectInputStream处理外部传入的数据;反序列化时必须指定明确的目标类型。
文件上传漏洞是Web应用中最常见的getshell漏洞,黑客可通过该漏洞上传webshell等恶意文件,直接控制服务器。
文件上传漏洞的核心原理,是应用对用户上传的文件未执行严格的校验,允许用户上传可执行的脚本文件,并且将文件存储到Web可访问的目录中,黑客可通过URL访问该文件,执行恶意脚本。
错误示例,文件上传漏洞:
package com.jam.demo.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
/**
* 文件上传漏洞示例控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
@Tag(name = "文件上传示例", description = "文件上传漏洞示例接口")
publicclass FileUploadDemoController {
/**
* 错误示例:文件上传漏洞
* 未校验文件类型,直接使用原始文件名存储到Web可访问目录
*/
@PostMapping("/upload/error")
@Operation(summary = "文件上传接口(错误示例)", description = "存在文件上传漏洞的接口")
public String uploadError(@RequestParam("file") MultipartFile file) {
try {
String fileName = file.getOriginalFilename();
String uploadPath = System.getProperty("user.dir") + "/src/main/resources/static/upload/";
File destFile = new File(uploadPath + fileName);
file.transferTo(destFile);
return"文件上传成功,访问地址:/upload/" + fileName;
} catch (Exception e) {
log.error("文件上传失败", e);
return"文件上传失败";
}
}
}
该接口没有任何文件校验,黑客可以上传.jsp、.jspx等脚本文件,然后通过URL访问该文件,执行恶意代码,控制服务器。
正确修复示例,多层校验+安全存储:
package com.jam.demo.controller;
import com.google.common.io.Files;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* 文件上传安全示例控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
@Tag(name = "文件上传示例", description = "文件上传漏洞示例接口")
publicclass FileUploadDemoController {
privatestaticfinal List<String> ALLOW_FILE_SUFFIX = Arrays.asList("jpg", "jpeg", "png", "gif", "pdf");
privatestaticfinallong MAX_FILE_SIZE = 10 * 1024 * 1024L;
privatestaticfinal String UPLOAD_ROOT_PATH = "/data/upload/";
/**
* 正确示例:安全的文件上传接口
* 多层白名单校验+安全存储+权限控制
* @param file 上传的文件
* @return 上传结果
*/
@PostMapping("/upload/safe")
@Operation(summary = "文件上传接口(安全示例)", description = "修复文件上传漏洞的安全接口")
public String uploadSafe(@RequestParam("file") MultipartFile file) {
if (ObjectUtils.isEmpty(file) || file.isEmpty()) {
return"上传的文件不能为空";
}
if (file.getSize() > MAX_FILE_SIZE) {
return"文件大小不能超过10MB";
}
String originalFileName = file.getOriginalFilename();
if (!org.springframework.util.StringUtils.hasText(originalFileName)) {
return"文件名不能为空";
}
String fileSuffix = Files.getFileExtension(originalFileName).toLowerCase();
if (!ALLOW_FILE_SUFFIX.contains(fileSuffix)) {
return"仅允许上传jpg、jpeg、png、gif、pdf格式的文件";
}
try {
byte[] fileHeader = newbyte[8];
file.getInputStream().read(fileHeader);
if (!isValidFileHeader(fileHeader, fileSuffix)) {
return"文件格式非法";
}
} catch (Exception e) {
log.error("文件头校验失败", e);
return"文件校验失败";
}
String randomFileName = UUID.randomUUID().toString().replace("-", "") + "." + fileSuffix;
File destFile = new File(UPLOAD_ROOT_PATH + randomFileName);
if (!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
}
try {
file.transferTo(destFile);
} catch (Exception e) {
log.error("文件存储失败", e);
return"文件上传失败";
}
return"文件上传成功,文件ID:" + randomFileName;
}
/**
* 安全的文件下载接口
* 必须经过认证授权才能访问
* @param fileId 文件ID
* @param response 响应对象
*/
@GetMapping("/download")
@Operation(summary = "文件下载接口", description = "安全的文件下载接口,需授权访问")
public void downloadFile(@RequestParam String fileId, HttpServletResponse response) {
if (!org.springframework.util.StringUtils.hasText(fileId) || !fileId.matches("^[a-zA-Z0-9]{32}\\.[a-z]{3,4}$")) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
File file = new File(UPLOAD_ROOT_PATH + fileId);
if (!file.exists() || !file.isFile()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileId + "\"");
try (FileInputStream fis = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
byte[] buffer = newbyte[4096];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
} catch (Exception e) {
log.error("文件下载失败", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* 校验文件头是否合法
* @param fileHeader 文件头字节数组
* @param fileSuffix 文件后缀
* @return 是否合法
*/
private boolean isValidFileHeader(byte[] fileHeader, String fileSuffix) {
String headerHex = bytesToHex(fileHeader);
returnswitch (fileSuffix) {
case"jpg", "jpeg" -> headerHex.startsWith("FFD8FF");
case"png" -> headerHex.startsWith("89504E47");
case"gif" -> headerHex.startsWith("47494638");
case"pdf" -> headerHex.startsWith("25504446");
default -> false;
};
}
/**
* 字节数组转十六进制字符串
* @param bytes 字节数组
* @return 十六进制字符串
*/
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}
命令注入漏洞是高危的远程代码执行漏洞,黑客可通过该漏洞在服务器上执行任意系统命令,完全控制服务器。
命令注入漏洞的核心原理,是应用将用户可控的参数,未经处理直接拼接到系统命令中,通过Runtime.getRuntime().exec或ProcessBuilder执行,导致操作系统将用户输入的恶意内容,解析为系统命令执行。
错误示例,命令注入漏洞:
package com.jam.demo.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* 命令注入漏洞示例控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/cmd")
@RequiredArgsConstructor
@Tag(name = "命令执行示例", description = "命令注入漏洞示例接口")
publicclass CmdInjectDemoController {
/**
* 错误示例:命令注入漏洞
* 直接拼接用户输入的参数到系统命令中
*/
@GetMapping("/ping/error")
@Operation(summary = "ping测试接口(错误示例)", description = "存在命令注入漏洞的接口")
public String pingError(@RequestParam String ip) {
try {
String cmd = "ping -c 4 " + ip;
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder result = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
result.append(line).append("\n");
}
return result.toString();
} catch (Exception e) {
log.error("命令执行失败", e);
return"命令执行失败";
}
}
}
当用户输入127.0.0.1; rm -rf /时,拼接后的命令会先执行ping命令,然后执行rm -rf /命令,删除服务器上的所有文件。
正确修复示例,使用ProcessBuilder参数列表+白名单校验:
package com.jam.demo.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.List;
/**
* 命令注入安全示例控制器
* @author ken
*/
@Slf4j
@RestController
@RequestMapping("/cmd")
@RequiredArgsConstructor
@Tag(name = "命令执行示例", description = "命令注入漏洞示例接口")
publicclass CmdInjectDemoController {
/**
* 正确示例:安全的命令执行接口
* 使用ProcessBuilder参数列表,配合IP格式白名单校验
* @param ip 待ping的IP地址
* @return ping结果
*/
@GetMapping("/ping/safe")
@Operation(summary = "ping测试接口(安全示例)", description = "修复命令注入漏洞的安全接口")
public String pingSafe(@RequestParam String ip) {
if (!StringUtils.hasText(ip) || !isValidIpAddress(ip)) {
return"请输入合法的IP地址";
}
ProcessBuilder processBuilder = new ProcessBuilder(List.of("ping", "-c", "4", ip));
processBuilder.environment().clear();
try {
Process process = processBuilder.start();
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
StringBuilder result = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
result.append(line).append("\n");
}
process.waitFor();
return result.toString();
} catch (Exception e) {
log.error("命令执行失败", e);
return"命令执行失败";
}
}
/**
* 校验IP地址是否合法
* @param ip IP地址
* @return 是否合法
*/
private boolean isValidIpAddress(String ip) {
String ipv4Regex = "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$";
String ipv6Regex = "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$";
return ip.matches(ipv4Regex) || ip.matches(ipv6Regex);
}
}
代码安全审计不是零散的漏洞查找,而是一套标准化的全流程操作体系。本章节将梳理完整的代码安全审计流程,帮助开发者建立系统化的审计能力,避免遗漏高危漏洞。
完整的代码安全审计流程分为7个核心阶段,每个阶段都有明确的目标与输出物,确保审计的全面性与有效性。

审计准备阶段的核心目标,是明确审计范围,搭建审计环境,收集审计所需的基础信息。
依赖组件安全扫描是审计的第一步,也是投入产出比最高的环节。
代码静态初筛阶段的核心目标,是通过自动化工具快速找出代码中的高风险点,缩小人工审计的范围,提升审计效率。
这是审计的核心阶段,基于前面的初筛结果与业务梳理的高风险场景,对代码进行人工深度审计,找出自动化工具无法发现的逻辑漏洞与业务漏洞。
漏洞复现与验证阶段的核心目标,是确认漏洞的真实性与可利用性,避免误报,同时验证修复方案的有效性。
审计报告是审计工作的最终输出物,也是推动漏洞修复的核心依据。
回归审计是审计工作的闭环,核心目标是确认所有漏洞都得到了有效修复,没有引入新的安全隐患。
人工审计虽然全面,但效率较低,无法满足快速迭代的开发流程。建立自动化的安全审计工具链,将安全审计集成到CI/CD流程中,实现“提交即扫描、构建即检测”,是企业级应用安全防护的最佳实践。
工具名称 | 核心功能 | 适用场景 |
|---|---|---|
SonarQube | 静态代码扫描,支持安全漏洞、编码规范、代码质量检测 | 全量代码质量与安全审计,集成到CI/CD流程 |
FindSecBugs | 专门针对Java代码的安全漏洞扫描插件,可集成到SonarQube、IDEA中 | Java代码高危漏洞定向扫描 |
Dependency-Check | 第三方依赖组件漏洞扫描,匹配CVE、NVD漏洞库 | 依赖组件安全审计,定期漏洞扫描 |
Semgrep | 开源的静态代码分析工具,支持自定义规则,可快速扫描指定的漏洞模式 | 自定义漏洞规则扫描,定向风险排查 |
OWASP ZAP | 动态应用安全测试工具,支持主动扫描、被动扫描、漏洞利用 | 接口安全测试,运行时漏洞扫描 |
将安全审计集成到CI/CD流程中,实现安全左移,在开发阶段就发现并修复安全漏洞,避免漏洞流入生产环境。 核心集成流程:

集成核心规则:
将安全审计工具集成到开发者的本地IDE中,让开发者在编码阶段就发现并修复安全漏洞,是安全左移的核心环节。
安全审计的最终目标,是推动安全编码规范的落地,从源头减少安全漏洞的产生。本章节总结了Java开发中必须遵循的安全编码最佳实践,帮助开发者建立安全的编码习惯。
所有安全漏洞的根源,都是不可信的外部输入。输入校验是安全防护的第一道防线,必须遵循以下原则:
输出编码是防止XSS等注入类漏洞的核心手段,必须遵循以下原则:
最小权限原则是防止漏洞危害扩大的核心手段,必须贯穿应用的全生命周期:
数据是业务的核心资产,必须对敏感数据执行全生命周期的安全保护:
异常处理与日志,是排查问题的核心工具,也是最容易泄露敏感信息的环节,必须遵循以下原则:
第三方依赖是安全漏洞的重灾区,必须遵循以下原则:
代码安全没有一劳永逸的解决方案,安全审计也不是一次性的工作,而是一个持续迭代、持续优化的过程。对于Java开发者来说,建立安全的编码思维,掌握系统化的安全审计能力,将安全防护融入到开发的全流程中,才能从源头减少安全漏洞的产生,构建真正安全的企业级应用。