关于 security-audit
面向 Java 应用开发者的安全审查指南,基于 OWASP Top 10 和安全编码实践,涵盖输入验证、身份验证、依赖与配置检查。
主要功能
- 01OWASP Top 10 风险参考
- 02输入验证与注入防护
- 03身份验证与授权检查
- 04密钥、日志和反序列化建议
- 05依赖与安全配置检查
适用场景
- 01发布前审查 Java 应用安全性
- 02检查认证与授权代码
- 03查找 SQL 注入和 XSS 风险
- 04核对依赖漏洞及安全配置
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ decebals/ claude-code-java --skill security-audit安装后在 Claude Code 中输入/security-audit确认已加载。
附带文件README.md
详细介绍
安全审计技能
基于 OWASP Top 10 和安全编码实践的 Java 应用安全检查清单。
使用场景
- 安全代码审查
- 发布到生产环境之前
- 用户询问“安全”“漏洞”或“OWASP”
- 审查身份验证/授权代码
- 检查注入漏洞
OWASP Top 10 速查
| # | 风险 | Java 缓解措施 |
|---|---|---|
| A01 | 访问控制失效 | 基于角色进行检查,默认拒绝 |
| A02 | 加密机制失效 | 使用强算法,不硬编码密钥 |
| A03 | 注入 | 参数化查询,输入验证 |
| A04 | 不安全设计 | 威胁建模,安全默认设置 |
| A05 | 安全配置错误 | 禁用调试,设置安全标头 |
| A06 | 易受攻击的组件 | 扫描依赖项并更新 |
| A07 | 身份验证失效 | 强密码、多因素身份验证、会话管理 |
| A08 | 软件和数据完整性失效 | 验证签名,安全反序列化 |
| A09 | 日志记录和监控失效 | 记录安全事件,不记录敏感数据 |
| A10 | SSRF | 验证 URL,将域名列入允许列表 |
输入验证(所有框架)
Bean Validation(JSR 380)
适用于 Spring、Quarkus、Jakarta EE 和独立应用。
// ✅ GOOD: Validate at boundary
public class CreateUserRequest {
@NotNull(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be 3-50 characters")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Username can only contain letters, numbers, underscore")
private String username;
@NotNull
@Email(message = "Invalid email format")
private String email;
@NotNull
@Size(min = 8, max = 100)
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$",
message = "Password must contain uppercase, lowercase, and number")
private String password;
@Min(value = 0, message = "Age cannot be negative")
@Max(value = 150, message = "Invalid age")
private Integer age;
}
// Controller/Resource - trigger validation
public Response createUser(@Valid CreateUserRequest request) {
// request is already validated
}
自定义验证器
// Custom annotation
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SafeHtmlValidator.class)
public @interface SafeHtml {
String message() default "Contains unsafe HTML";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Validator implementation
public class SafeHtmlValidator implements ConstraintValidator<SafeHtml, String> {
private static final Pattern DANGEROUS_PATTERN = Pattern.compile(
"<script|javascript:|on\\w+\\s*=", Pattern.CASE_INSENSITIVE
);
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true;
return !DANGEROUS_PATTERN.matcher(value).find();
}
}
允许列表与阻止列表
// ❌ BAD: Blocklist (attackers find bypasses)
if (input.contains("<script>")) {
throw new ValidationException("Invalid input");
}
// ✅ GOOD: Allowlist (only permit known-good)
private static final Pattern SAFE_NAME = Pattern.compile("^[a-zA-Z\\s'-]{1,100}$");
if (!SAFE_NAME.matcher(input).matches()) {
throw new ValidationException("Invalid name format");
}
防止 SQL 注入
JPA/Hibernate(所有框架)
// ✅ GOOD: Parameterized queries
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
// ✅ GOOD: Criteria API
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);
query.where(cb.equal(user.get("email"), email)); // Safe
// ✅ GOOD: Named parameters
TypedQuery<User> query = entityManager.createQuery(
"SELECT u FROM User u WHERE u.status = :status", User.class);
query.setParameter("status", status); // Safe
// ❌ BAD: String concatenation
String jpql = "SELECT u FROM User u WHERE u.email = '" + email + "'"; // VULNERABLE!
原生查询
// ✅ GOOD: Parameterized native query
@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
User findByEmailNative(String email);
// ❌ BAD: Concatenated native query
String sql = "SELECT * FROM users WHERE email = '" + email + "'"; // VULNERABLE!
JDBC(纯 Java)
// ✅ GOOD: PreparedStatement
String sql = "SELECT * FROM users WHERE email = ? AND status = ?";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, email);
stmt.setString(2, status);
ResultSet rs = stmt.executeQuery();
}
// ❌ BAD: Statement with concatenation
String sql = "SELECT * FROM users WHERE email = '" + email + "'"; // VULNERABLE!
Statement stmt = connection.createStatement();
stmt.executeQuery(sql);
防止 XSS
输出编码
// ✅ GOOD: Use templating engine's auto-escaping
// Thymeleaf - auto-escapes by default
<p th:text="${userInput}">...</p> // Safe
// To display HTML (dangerous, use carefully):
<p th:utext="${trustedHtml}">...</p> // Only for trusted content!
// ✅ GOOD: Manual encoding when needed
import org.owasp.encoder.Encode;
String safe = Encode.forHtml(userInput);
String safeJs = Encode.forJavaScript(userInput);
String safeUrl = Encode.forUriComponent(userInput);
OWASP Encoder 的 Maven 依赖项:
<dependency>
<groupId>org.owasp.encoder</groupId>
<artifactId>encoder</artifactId>
<version>1.2.3</version>
</dependency>
内容安全策略
// Add CSP header to prevent inline scripts
// Spring Boot
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'; style-src 'self'")
)
);
return http.build();
}
}
// Servlet Filter (works everywhere)
@WebFilter("/*")
public class SecurityHeadersFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Content-Security-Policy", "default-src 'self'");
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
chain.doFilter(req, res);
}
}
CSRF 防护
Spring Security
// CSRF enabled by default for browser clients
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// For REST APIs with JWT (stateless) - can disable CSRF
.csrf(csrf -> csrf.disable())
// For browser apps with sessions - keep CSRF enabled
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}
Quarkus
# application.properties
quarkus.http.csrf.enabled=true
quarkus.http.csrf.cookie-name=XSRF-TOKEN
身份验证与授权
密码存储
// ✅ GOOD: Use BCrypt or Argon2
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
// BCrypt (widely supported)
PasswordEncoder encoder = new BCryptPasswordEncoder(12); // strength 12
String hash = encoder.encode(rawPassword);
boolean matches = encoder.matches(rawPassword, hash);
// Argon2 (recommended for new projects)
PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String hash = encoder.encode(rawPassword);
// ❌ BAD: MD5, SHA1, SHA256 without salt
String hash = DigestUtils.md5Hex(password); // NEVER for passwords!
授权检查
// ✅ GOOD: Check authorization at service layer
@Service
public class DocumentService {
public Document getDocument(Long documentId, User currentUser) {
Document doc = documentRepository.findById(documentId)
.orElseThrow(() -> new NotFoundException("Document not found"));
// Authorization check
if (!doc.getOwnerId().equals(currentUser.getId()) &&
!currentUser.hasRole("ADMIN")) {
throw new AccessDeniedException("Not authorized to access this document");
}
return doc;
}
}
// ❌ BAD: Only check at controller level, trust user input
@GetMapping("/documents/{id}")
public Document getDocument(@PathVariable Long id) {
return documentRepository.findById(id).orElseThrow(); // No auth check!
}
Spring Security 注解
@PreAuthorize("hasRole('ADMIN')")
public void adminOnly() { }
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
public void ownDataOnly(Long userId) { }
@PreAuthorize("@authService.canAccess(#documentId, authentication)")
public Document getDocument(Long documentId) { }
密钥管理
切勿硬编码密钥
// ❌ BAD: Hardcoded secrets
private static final String API_KEY = "sk-1234567890abcdef";
private static final String DB_PASSWORD = "admin123";
// ✅ GOOD: Environment variables
String apiKey = System.getenv("API_KEY");
// ✅ GOOD: External configuration
@Value("${api.key}")
private String apiKey;
// ✅ GOOD: Secrets manager
@Autowired
private SecretsManager secretsManager;
String apiKey = secretsManager.getSecret("api-key");
配置文件
# ✅ GOOD: Reference environment variables
spring:
datasource:
password: ${DB_PASSWORD}
api:
key: ${API_KEY}
# ❌ BAD: Hardcoded in application.yml
spring:
datasource:
password: admin123 # NEVER!
.gitignore
# Never commit these
.env
*.pem
*.key
*credentials*
*secret*
application-local.yml
安全反序列化
避免使用 Java 序列化
// ❌ DANGEROUS: Java ObjectInputStream
ObjectInputStream ois = new ObjectInputStream(untrustedInput);
Object obj = ois.readObject(); // Remote Code Execution risk!
// ✅ GOOD: Use JSON with Jackson
ObjectMapper mapper = new ObjectMapper();
// Disable dangerous features
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
); // Be careful with polymorphic types!
User user = mapper.readValue(json, User.class);
Jackson 安全性
// ✅ Configure Jackson safely
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Prevent unknown properties exploitation
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Don't allow class type in JSON (prevents gadget attacks)
mapper.deactivateDefaultTyping();
return mapper;
}
}
依赖项安全
OWASP Dependency Check
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>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
</configuration>
</plugin>
运行:
mvn dependency-check:check
# Report: target/dependency-check-report.html
保持依赖项更新
# Check for updates
mvn versions:display-dependency-updates
# Update to latest
mvn versions:use-latest-releases
安全标头
推荐的标头
| 标头 | 值 | 用途 |
|---|---|---|
Content-Security-Policy | default-src 'self' | 防止 XSS |
X-Content-Type-Options | nosniff | 防止 MIME 嗅探 |
X-Frame-Options | DENY | 防止点击劫持 |
Strict-Transport-Security | max-age=31536000 | 强制使用 HTTPS |
X-XSS-Protection | 1; mode=block | 旧版 XSS 过滤器 |
Spring Boot 配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000))
.contentTypeOptions(Customizer.withDefaults())
);
return http.build();
}
记录安全事件
// ✅ Log security-relevant events
log.info("User login successful", kv("userId", userId), kv("ip", clientIp));
log.warn("Failed login attempt", kv("username", username), kv("ip", clientIp), kv("attempt", attemptCount));
log.warn("Access denied", kv("userId", userId), kv("resource", resourceId), kv("action", action));
log.error("Authentication failure", kv("reason", reason), kv("ip", clientIp));
// ❌ NEVER log sensitive data
log.info("Login: user={}, password={}", username, password); // NEVER!
log.debug("Request body: {}", requestWithCreditCard); // NEVER!
安全检查清单
代码审查
- 使用允许列表模式验证输入
- SQL 查询使用参数(不进行字符串拼接)
- 根据上下文对输出进行编码(HTML、JS、URL)
- 在服务层检查授权
- 不硬编码密钥
- 使用 BCrypt/Argon2 对密码进行哈希处理
- 不记录敏感数据
- 启用 CSRF 防护(适用于浏览器应用)
配置
- 强制使用 HTTPS
- 配置安全标头
- 在生产环境中禁用调试/开发功能
- 更改默认凭据
- 错误消息不会泄露内部详细信息
依赖项
- 不存在已知漏洞(通过 OWASP 检查)
- 依赖项保持最新
- 移除不必要的依赖项
相关技能
java-code-review- 通用代码审查maven-dependency-audit- 依赖项漏洞扫描logging-patterns- 安全日志记录实践
评论
支持 Markdown