Dopo anni di sviluppo enterprise, ho distillato le best practice di sicurezza che applico in ogni progetto: da input validation alla gestione dei secret, ecco la mia checklist.
Dopo 6 anni di sviluppo enterprise, ho visto di tutto: SQL injection in produzione, password in chiaro nei log, endpoint senza autenticazione. Per evitare tutto ciò, ecco le regole che seguo religiosamente in ogni progetto Spring Boot.
1Input Validation: Mai fidarsi dell'utente
La prima regola è semplice: ogni input è potenzialmente malevolo. Non importa se arriva da un form, un'API, o un file...Valida sempre!
In Spring Boot uso una combinazione di Bean Validation e validatori custom:
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<User> createUser(
@Valid @RequestBody UserDTO dto) {
// La validazione è già avvenuta grazie a @Valid
return userService.create(dto);
}
}
public record UserDTO(
@NotBlank
@Size(min = 2, max = 50)
String name,
@Email
@NotBlank
String email,
@Pattern(regexp = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d).{8,}$")
String password
) {}2SQL Injection: Prepared Statement sempre
Nel 2025, trovare SQL injection in produzione è imbarazzante, eppure succede ancora. La regola è semplice: mai concatenare stringhe per costruire query.
Usa sempre JPA o Prepared Statement:
// ✅ Corretto: JPA Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
}
// ✅ Corretto: Named Parameters
jdbcTemplate.query(
"SELECT * FROM users WHERE status = ?",
new Object[]{status},
userRowMapper
);3Gestione dei Secret: Mai nel codice
I secret non devono mai apparire nel codice sorgente, uso sempre variabili d'ambiente o secret manager.
# application.yml - MAI così
spring:
datasource:
password: miaPasswordSegreta123 # ❌ NO!
# application.yml - Sempre così
spring:
datasource:
password: ${DB_PASSWORD} # ✅ Variabile d'ambiente4CORS: Configurazione esplicita
Mai lasciare CORS aperto a tutti! Configura esplicitamente le origini permesse:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(
"https://miosito.com",
"https://app.miosito.com"
)
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(3600);
}
}5Rate Limiting: Protezione da abuse
Ogni API pubblica dovrebbe avere rate limiting. Uso Bucket4j o Redis:
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Bucket bucket = Bucket.builder()
.addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
.build();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
if (bucket.tryConsume(1)) {
chain.doFilter(request, response);
} else {
response.setStatus(429);
response.getWriter().write("Too many requests");
}
}
}6Logging Sicuro: Mai loggare dati sensibili
I log sono una miniera d'oro per gli attaccanti. Mai loggare:
// ❌ MAI
log.info("User login: email={}, password={}", email, password);
// ✅ SEMPRE
log.info("User login attempt: email={}", maskEmail(email));7HTTPS Everywhere
In produzione, sempre e solo HTTPS:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.requiresChannel(channel ->
channel.anyRequest().requiresSecure()
)
// ... altre configurazioni
;
return http.build();
}
}8Security Headers
Aggiungo sempre gli header di sicurezza:
@Configuration
public class SecurityHeadersConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("Strict-Transport-Security",
"max-age=31536000; includeSubDomains");
return true;
}
});
}
}9Dependency Check: Vulnerabilità note
Uso OWASP Dependency Check nel build:
<style="color: #f07178;">plugin>
<style="color: #f07178;">groupId>org.owasp</style="color: #f07178;">groupId>
<style="color: #f07178;">artifactId>dependency-check-maven</style="color: #f07178;">artifactId>
<style="color: #f07178;">version>8.4.0</style="color: #f07178;">version>
<style="color: #f07178;">executions>
<style="color: #f07178;">execution>
<style="color: #f07178;">goals>
<style="color: #f07178;">goal>check</style="color: #f07178;">goal>
</style="color: #f07178;">goals>
</style="color: #f07178;">execution>
</style="color: #f07178;">executions>
</style="color: #f07178;">plugin>10Error Handling: Mai esporre dettagli
In produzione, gli errori non devono mai esporre stack trace o dettagli interni:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
// Log interno con dettagli
log.error("Errore interno", ex);
// Risposta generica all'utente
return ResponseEntity
.status(500)
.body(new ErrorResponse(
"Si è verificato un errore. Riprova più tardi.",
Instant.now()
));
}
}Conclusione
La sicurezza non è un feature da aggiungere alla fine, ma un mindset da avere dall'inizio. Queste 10 regole sono il minimo indispensabile per ogni progetto Spring Boot in produzione.
Vuoi approfondire? Scrivimi, sono sempre felice di discutere di sicurezza applicativa.