第 33 章:应用安全
学习目标
- 掌握依赖漏洞扫描与修复
- 学会反序列化漏洞防护
- 实现文件上传、API 防护
- 避免代码注入、依赖中毒的坑
一、依赖漏洞扫描
bash
# 1. OWASP Dependency-Check
mvn org.owasp:dependency-check-maven:check
# 2. Snyk
npx snyk test
# 3. Trivy
trivy image myapp:latestxml
<!-- pom.xml 配置 OWASP -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.0</version>
<configuration>
<failBuildOnCVSS>7.0</failBuildOnCVSS>
</configuration>
</plugin>java
// 漏洞库:Log4j 2.14.0 以前有 RCE(2021/12)
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version> <!-- 升级到 2.17.1+ -->
</dependency>yaml
# CI/CD 集成
stages:
- build
- test
- security_scan
security_scan:
stage: security_scan
script:
- mvn org.owasp:dependency-check-maven:check
- trivy image myapp:latest
artifacts:
reports:
dependency_check: 'dependency-check-report.xml'⚠️ 坑 1:依赖漏洞无人看,Java 反序列化漏洞躺 3 年。CI/CD 集成扫描,高危漏洞阻断发布。
二、反序列化漏洞
java
// ❌ 危险:ObjectInputStream 接收任意对象
public Object deserialize(byte[] bytes) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
return ois.readObject(); // 攻击者传入恶意对象,执行任意代码
}
// ✅ 修复:白名单 + 类型检查
public <T> T deserialize(byte[] bytes, Class<T> expected) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (!expected.isAssignableFrom(super.resolveClass(desc))) {
throw new InvalidClassException("Unexpected class: " + desc.getName());
}
return super.resolveClass(desc);
}
};
return expected.cast(ois.readObject());
}java
// ✅ 推荐:用 JSON 替代 Java 序列化
public class UserDto {
private Long id;
private String name;
}
ObjectMapper mapper = new ObjectMapper();
UserDto user = mapper.readValue(json, UserDto.class);⚠️ 坑 2:Redis 用了 Java 序列化,JDK 17 默认改了序列化机制,反序列化失败或兼容性问题。统一用 JSON。
三、文件上传安全
java
// 1. 类型校验
public class FileUploadValidator {
private static final Set<String> ALLOWED_TYPES = Set.of("image/jpeg", "image/png", "application/pdf");
private static final long MAX_SIZE = 10 * 1024 * 1024; // 10MB
public void validate(MultipartFile file) {
// 1. 大小
if (file.getSize() > MAX_SIZE) {
throw new FileTooLargeException();
}
// 2. MIME 类型
String contentType = file.getContentType();
if (!ALLOWED_TYPES.contains(contentType)) {
throw new UnsupportedFileTypeException();
}
// 3. 真实类型(魔数)
try (InputStream is = file.getInputStream()) {
byte[] magic = new byte[8];
is.read(magic);
if (!isValidImage(magic)) {
throw new FileContentException();
}
}
}
private boolean isValidImage(byte[] magic) {
// JPEG: FF D8 FF
return magic[0] == (byte) 0xFF && magic[1] == (byte) 0xD8 && magic[2] == (byte) 0xFF;
}
}java
// 2. 文件名净化
public String sanitizeFilename(String filename) {
// 防止 ../ 路径穿越
String name = filename.replaceAll("[\\\\/:*?\"<>|]", "");
// 防止脚本文件
if (name.endsWith(".jsp") || name.endsWith(".php") || name.endsWith(".exe")) {
throw new UnsupportedFileTypeException();
}
return name;
}
// 3. 存储到对象存储,不直接存应用服务器
String url = ossService.upload("user-files/" + UUID.randomUUID(), file.getBytes());四、API 防护
java
// 1. 限流
@RateLimiter(limit = 100, period = 60)
@GetMapping("/api/order/{id}")
public Order getOrder(@PathVariable Long id) {
return orderRepository.findById(id).orElseThrow();
}
// 2. 防爬虫
@BotDetection // 自定义注解,校验 UA / 行为
public List<Product> listProducts() {
return productRepository.findAll();
}
public class BotDetectionAspect {
@Around("@annotation(BotDetection)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String userAgent = req.getHeader("User-Agent");
if (isBot(userAgent)) {
throw new BotDetectedException();
}
return pjp.proceed();
}
}java
// 3. 重要接口加图形验证码 / 短信验证码
@PostMapping("/api/login")
public TokenDto login(@RequestBody LoginRequest req, @RequestParam String captcha) {
if (!captchaService.verify(req.getUsername(), captcha)) {
throw new InvalidCaptchaException();
}
return authService.login(req);
}五、敏感信息保护
java
// 1. 日志脱敏
public class SensitiveDataMasker {
private static final Pattern PHONE = Pattern.compile("(\\d{3})\\d{4}(\\d{4})");
private static final Pattern ID_CARD = Pattern.compile("(\\d{4})\\d{10}(\\w{4})");
public static String mask(String input) {
if (input == null) return null;
input = PHONE.matcher(input).replaceAll("$1****$2");
input = ID_CARD.matcher(input).replaceAll("$1**********$2");
return input;
}
}
// Logback 过滤器
public class MaskingFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
String msg = event.getFormattedMessage();
if (msg.contains("password") || msg.contains("idCard")) {
return FilterReply.DENY; // 整条日志丢弃
}
return FilterReply.NEUTRAL;
}
}java
// 2. 异常信息不暴露
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDto> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.status(500).body(new ErrorDto("INTERNAL_ERROR", "系统繁忙,请稍后再试"));
// ❌ 不要返回 e.getMessage(),可能暴露 SQL、文件名
}java
// 3. 配置文件脱敏
@Value("${spring.datasource.password}")
private String dbPassword;
@Configuration
public class SensitiveConfig {
@Bean
public SensitiveDataMessageConverter sensitiveConverter() {
return new SensitiveDataMessageConverter();
}
}⚠️ 坑 3:日志打印 SQL:
select * from user where password = '123456',密码明文入日志。参数化日志 + 脱敏中间件。
六、容器与镜像安全
dockerfile
# 1. 最小化镜像
FROM eclipse-temurin:17-jre-alpine
# 不要用 latest,固定版本
# 2. 非 root 用户
RUN adduser -D appuser
USER appuser
# 3. 复制文件
COPY --chown=appuser:appuser app.jar /app/
# 4. 启动
ENTRYPOINT ["java", "-jar", "/app/app.jar"]yaml
# 5. 镜像扫描
stages:
- build
- scan
- deploy
scan:
stage: scan
script:
- trivy image --severity HIGH,CRITICAL myapp:latest
allow_failure: false七、依赖锁定
xml
<!-- pom.xml 锁定版本 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.0</version>
</dependency>bash
# npm 锁定
npm ci # 用 package-lock.jsonyaml
# 私有源
<repositories>
<repository>
<id>private</id>
<url>https://nexus.example.com/repository/maven-public/</url>
</repository>
</repositories>八、CSP 跨域
java
// 1. CORS 严格限制
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com")); // 不用 "*"
config.setAllowedMethods(List.of("GET", "POST"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
// 2. CSP
http.headers(csp -> csp.policyDirectives(
"default-src 'self'; " +
"script-src 'self' 'nonce-${nonce}'; " +
"style-src 'self' 'nonce-${nonce}'; " +
"img-src 'self' data:; " +
"object-src 'none'"
));九、安全扫描工具
bash
# 1. SAST(静态分析)
mvn sonar:sonar
# 2. DAST(动态分析)
zap-cli quick-scan https://app.example.com
# 3. 密钥扫描
gitleaks detect
# 4. 容器扫描
trivy image myapp:latest十、安全开发规范
text
1. 所有输入必须校验(白名单优于黑名单)
2. 所有输出必须转义(防 XSS)
3. 敏感信息不写日志(密码、密钥、身份证)
4. 错误信息不暴露内部细节
5. 第三方依赖必须扫描
6. 容器以非 root 运行
7. 密钥独立存储,定期轮换⚠️ 坑 4:开发规范文档躺在 Wiki,没人看。CI 流水线 + Lint 工具,把规范变成自动检查。
本章小结
| 维度 | 措施 |
|---|---|
| 依赖 | 漏洞扫描 + 锁定版本 |
| 序列化 | 替代方案(JSON) |
| 上传 | 类型 + 大小 + 真实内容 |
| API | 限流 + 验证码 |
| 容器 | 非 root + 镜像扫描 |
| 工具 | 用途 |
|---|---|
| OWASP Dependency-Check | 依赖漏洞 |
| Snyk | 多语言依赖 |
| Trivy | 容器扫描 |
| SonarQube | 代码质量 |
| Gitleaks | 密钥扫描 |
动手练习
- 依赖扫描:用 OWASP Dependency-Check 扫描项目,修复高危漏洞
- 反序列化:把 Java 序列化改为 JSON,避免反序列化漏洞
- 文件上传:实现校验:类型/大小/魔数,存储到 OSS
- 日志脱敏:用 Logback Filter 屏蔽密码字段
下一章:第 34 章:审计与合规 →