Input Validation
Never trust user input. Validate all data at system boundaries.
Whitelisting Approach
// GOOD: Whitelist validation - define what IS allowed
public String sanitizeUsername(String username) {
if (username == null || username.isBlank()) {
throw new ValidationException("Username required");
}
// Only allow alphanumeric and underscore
if (!username.matches("^[a-zA-Z0-9_]{3,20}$")) {
throw new ValidationException("Invalid username format");
}
return username;
}
// Bean Validation annotations
public class UserRegistration {
@NotBlank
@Size(min = 3, max = 20)
@Pattern(regexp = "^[a-zA-Z0-9_]+$")
private String username;
@Email
@NotBlank
private String email;
@Size(min = 8, max = 100)
private String password;
}
SQL Injection Prevention
// BAD: SQL Injection vulnerable!
public User findByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
// Attacker input: ' OR '1'='1' --
// Results in: SELECT * FROM users WHERE username = '' OR '1'='1' --'
return jdbcTemplate.queryForObject(sql, userRowMapper);
}
// GOOD: Parameterized query
public User findByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = ?";
return jdbcTemplate.queryForObject(sql, userRowMapper, username);
}
// GOOD: JPA/Hibernate with named parameters
@Query("SELECT u FROM User u WHERE u.username = :username")
Optional<User> findByUsername(@Param("username") String username);
// GOOD: Criteria API
public List<User> findByUsername(String username) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
query.where(cb.equal(root.get("username"), username));
return em.createQuery(query).getResultList();
}
Cross-Site Scripting (XSS) Prevention
// BAD: Directly outputting user content
@GetMapping("/profile")
public String profile(Model model) {
String bio = user.getBio(); // Could contain: <script>alert('XSS')</script>
model.addAttribute("bio", bio);
return "profile"; // Rendered without escaping!
}
// GOOD: Thymeleaf auto-escapes by default
// In template: <p th:text="${bio}"></p>
// Output: <script>alert('XSS')</script>
// For APIs - encode output
public String sanitizeForHtml(String input) {
return HtmlUtils.htmlEscape(input);
}
// Content Security Policy header
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
);
return http.build();
}
Password Security
// NEVER store plain text passwords!
// GOOD: Use BCrypt (or Argon2)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Cost factor
}
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
public void register(UserRegistration registration) {
User user = new User();
user.setUsername(registration.getUsername());
// Hash password before storing
user.setPasswordHash(passwordEncoder.encode(registration.getPassword()));
userRepository.save(user);
}
public boolean authenticate(String username, String password) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new AuthenticationException("Invalid credentials"));
// Compare with hashed password
return passwordEncoder.matches(password, user.getPasswordHash());
}
}
// Password requirements validation
public void validatePassword(String password) {
if (password.length() < 8) {
throw new ValidationException("Password must be at least 8 characters");
}
if (!password.matches(".*[A-Z].*")) {
throw new ValidationException("Password must contain uppercase letter");
}
if (!password.matches(".*[0-9].*")) {
throw new ValidationException("Password must contain a digit");
}
}
Secrets Management
// BAD: Hardcoded credentials
String apiKey = "sk_live_abc123xyz789"; // Never do this!
// GOOD: Environment variables
String apiKey = System.getenv("API_KEY");
// GOOD: Spring externalized configuration
@Value("${api.secret-key}")
private String apiSecretKey;
// GOOD: Spring Cloud Vault integration
@Configuration
public class VaultConfig {
@Value("${vault.database.password}")
private String dbPassword;
}
// Sensitive data in logs
// BAD
log.info("User {} logged in with password {}", username, password);
// GOOD: Never log sensitive data
log.info("User {} logged in", username);
// Clear sensitive data from memory when done
char[] password = getPassword();
try {
authenticate(password);
} finally {
Arrays.fill(password, '\0'); // Overwrite password
}
CSRF Protection
// Spring Security CSRF protection (enabled by default)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF is enabled by default
// For APIs using JWT/tokens, you might disable it:
// .csrf(csrf -> csrf.disable())
// Configure for SPA with cookie-based CSRF
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
// In Thymeleaf forms - token included automatically
// <form th:action="@{/submit}" method="post">
// <!-- Hidden CSRF token field added automatically -->
// </form>
Secure Random Numbers
// BAD: Predictable random - NOT for security
Random random = new Random();
String token = String.valueOf(random.nextLong()); // Predictable!
// GOOD: Cryptographically secure random
SecureRandom secureRandom = new SecureRandom();
byte[] tokenBytes = new byte[32];
secureRandom.nextBytes(tokenBytes);
String token = Base64.getUrlEncoder().encodeToString(tokenBytes);
// For session IDs, tokens, etc.
public String generateSecureToken() {
byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
Secure File Handling
// BAD: Path traversal vulnerability
public File getFile(String filename) {
// Attacker input: "../../../etc/passwd"
return new File("/uploads/" + filename);
}
// GOOD: Validate and normalize path
public Path getFile(String filename) {
Path basePath = Paths.get("/uploads").toAbsolutePath().normalize();
Path filePath = basePath.resolve(filename).normalize();
// Ensure file is within base directory
if (!filePath.startsWith(basePath)) {
throw new SecurityException("Invalid file path");
}
return filePath;
}
// Validate file uploads
public void handleUpload(MultipartFile file) {
// Check file type by content, not just extension
String contentType = file.getContentType();
if (!ALLOWED_TYPES.contains(contentType)) {
throw new ValidationException("File type not allowed");
}
// Limit file size
if (file.getSize() > MAX_FILE_SIZE) {
throw new ValidationException("File too large");
}
// Generate new filename
String safeFilename = UUID.randomUUID() + getExtension(file);
}
Security Headers
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
// Prevent clickjacking
.frameOptions(frame -> frame.deny())
// XSS protection (mostly for older browsers)
.xssProtection(xss -> xss.enable())
// Prevent MIME type sniffing
.contentTypeOptions(content -> {})
// HTTP Strict Transport Security
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
)
// Content Security Policy
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
);
return http.build();
}
Security Checklist
OWASP Top 10 Quick Reference
- Injection - Use parameterized queries
- Broken Authentication - Strong passwords, MFA, secure sessions
- Sensitive Data Exposure - Encrypt at rest and in transit
- XML External Entities - Disable DTDs, use safe parsers
- Broken Access Control - Check authorization on every request
- Security Misconfiguration - Secure defaults, remove unused features
- Cross-Site Scripting - Escape output, use CSP
- Insecure Deserialization - Avoid Java serialization for untrusted data
- Components with Vulnerabilities - Keep dependencies updated
- Insufficient Logging - Log security events, monitor for attacks