$npx -y skills add decebals/claude-code-java --skill security-auditJava security checklist covering OWASP Top 10, input validation, injection prevention, and secure coding. Works with Spring, Quarkus, Jakarta EE, and plain Java. Use when reviewing code security, before releases, or when user asks about vulnerabilities.
| 1 | # Security Audit Skill |
| 2 | |
| 3 | Security checklist for Java applications based on OWASP Top 10 and secure coding practices. |
| 4 | |
| 5 | ## When to Use |
| 6 | - Security code review |
| 7 | - Before production releases |
| 8 | - User asks about "security", "vulnerability", "OWASP" |
| 9 | - Reviewing authentication/authorization code |
| 10 | - Checking for injection vulnerabilities |
| 11 | |
| 12 | --- |
| 13 | |
| 14 | ## OWASP Top 10 Quick Reference |
| 15 | |
| 16 | | # | Risk | Java Mitigation | |
| 17 | |---|------|-----------------| |
| 18 | | A01 | Broken Access Control | Role-based checks, deny by default | |
| 19 | | A02 | Cryptographic Failures | Use strong algorithms, no hardcoded secrets | |
| 20 | | A03 | Injection | Parameterized queries, input validation | |
| 21 | | A04 | Insecure Design | Threat modeling, secure defaults | |
| 22 | | A05 | Security Misconfiguration | Disable debug, secure headers | |
| 23 | | A06 | Vulnerable Components | Dependency scanning, updates | |
| 24 | | A07 | Authentication Failures | Strong passwords, MFA, session management | |
| 25 | | A08 | Data Integrity Failures | Verify signatures, secure deserialization | |
| 26 | | A09 | Logging Failures | Log security events, no sensitive data | |
| 27 | | A10 | SSRF | Validate URLs, allowlist domains | |
| 28 | |
| 29 | --- |
| 30 | |
| 31 | ## Input Validation (All Frameworks) |
| 32 | |
| 33 | ### Bean Validation (JSR 380) |
| 34 | |
| 35 | Works in Spring, Quarkus, Jakarta EE, and standalone. |
| 36 | |
| 37 | ```java |
| 38 | // ✅ GOOD: Validate at boundary |
| 39 | public class CreateUserRequest { |
| 40 | |
| 41 | @NotNull(message = "Username is required") |
| 42 | @Size(min = 3, max = 50, message = "Username must be 3-50 characters") |
| 43 | @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "Username can only contain letters, numbers, underscore") |
| 44 | private String username; |
| 45 | |
| 46 | @NotNull |
| 47 | @Email(message = "Invalid email format") |
| 48 | private String email; |
| 49 | |
| 50 | @NotNull |
| 51 | @Size(min = 8, max = 100) |
| 52 | @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$", |
| 53 | message = "Password must contain uppercase, lowercase, and number") |
| 54 | private String password; |
| 55 | |
| 56 | @Min(value = 0, message = "Age cannot be negative") |
| 57 | @Max(value = 150, message = "Invalid age") |
| 58 | private Integer age; |
| 59 | } |
| 60 | |
| 61 | // Controller/Resource - trigger validation |
| 62 | public Response createUser(@Valid CreateUserRequest request) { |
| 63 | // request is already validated |
| 64 | } |
| 65 | ``` |
| 66 | |
| 67 | ### Custom Validators |
| 68 | |
| 69 | ```java |
| 70 | // Custom annotation |
| 71 | @Target({ElementType.FIELD}) |
| 72 | @Retention(RetentionPolicy.RUNTIME) |
| 73 | @Constraint(validatedBy = SafeHtmlValidator.class) |
| 74 | public @interface SafeHtml { |
| 75 | String message() default "Contains unsafe HTML"; |
| 76 | Class<?>[] groups() default {}; |
| 77 | Class<? extends Payload>[] payload() default {}; |
| 78 | } |
| 79 | |
| 80 | // Validator implementation |
| 81 | public class SafeHtmlValidator implements ConstraintValidator<SafeHtml, String> { |
| 82 | |
| 83 | private static final Pattern DANGEROUS_PATTERN = Pattern.compile( |
| 84 | "<script|javascript:|on\\w+\\s*=", Pattern.CASE_INSENSITIVE |
| 85 | ); |
| 86 | |
| 87 | @Override |
| 88 | public boolean isValid(String value, ConstraintValidatorContext context) { |
| 89 | if (value == null) return true; |
| 90 | return !DANGEROUS_PATTERN.matcher(value).find(); |
| 91 | } |
| 92 | } |
| 93 | ``` |
| 94 | |
| 95 | ### Allowlist vs Blocklist |
| 96 | |
| 97 | ```java |
| 98 | // ❌ BAD: Blocklist (attackers find bypasses) |
| 99 | if (input.contains("<script>")) { |
| 100 | throw new ValidationException("Invalid input"); |
| 101 | } |
| 102 | |
| 103 | // ✅ GOOD: Allowlist (only permit known-good) |
| 104 | private static final Pattern SAFE_NAME = Pattern.compile("^[a-zA-Z\\s'-]{1,100}$"); |
| 105 | |
| 106 | if (!SAFE_NAME.matcher(input).matches()) { |
| 107 | throw new ValidationException("Invalid name format"); |
| 108 | } |
| 109 | ``` |
| 110 | |
| 111 | --- |
| 112 | |
| 113 | ## SQL Injection Prevention |
| 114 | |
| 115 | ### JPA/Hibernate (All Frameworks) |
| 116 | |
| 117 | ```java |
| 118 | // ✅ GOOD: Parameterized queries |
| 119 | @Query("SELECT u FROM User u WHERE u.email = :email") |
| 120 | Optional<User> findByEmail(@Param("email") String email); |
| 121 | |
| 122 | // ✅ GOOD: Criteria API |
| 123 | CriteriaBuilder cb = entityManager.getCriteriaBuilder(); |
| 124 | CriteriaQuery<User> query = cb.createQuery(User.class); |
| 125 | Root<User> user = query.from(User.class); |
| 126 | query.where(cb.equal(user.get("email"), email)); // Safe |
| 127 | |
| 128 | // ✅ GOOD: Named parameters |
| 129 | TypedQuery<User> query = entityManager.createQuery( |
| 130 | "SELECT u FROM User u WHERE u.status = :status", User.class); |
| 131 | query.setParameter("status", status); // Safe |
| 132 | |
| 133 | // ❌ BAD: String concatenation |
| 134 | String jpql = "SELECT u FROM User u WHERE u.email = '" + email + "'"; // VULNERABLE! |
| 135 | ``` |
| 136 | |
| 137 | ### Native Queries |
| 138 | |
| 139 | ```java |
| 140 | // ✅ GOOD: Parameterized native query |
| 141 | @Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true) |
| 142 | User findByEmailNative(String email); |
| 143 | |
| 144 | // ❌ BAD: Concatenated native query |
| 145 | String sql = "SELECT * FROM users WHERE email = '" + email + "'"; // VULNERABLE! |
| 146 | ``` |
| 147 | |
| 148 | ### JDBC (Plain Java) |
| 149 | |
| 150 | ```java |
| 151 | // ✅ GOOD: PreparedStatement |
| 152 | String sql = "SELECT * FROM users WHERE email = ? AND status = ?"; |
| 153 | try (PreparedStatement stmt = connection.prepareStatement(sql)) { |
| 154 | stmt.setString(1, email); |
| 155 | stmt.setString(2, status); |
| 156 | Res |