TalentFlow — talent-api
Spring Boot 3.2 · Spring Security 6.2 · Java 21
6 Críticos 3 Mejoras Revisión: 18 Jun 2026
Vulnerabilidades críticas
6
Corrección inmediata
Mejoras recomendadas
3
Antes de producción
Items checklist
10
7 implementados
Score de seguridad
D → A
Tras aplicar fixes
🚨
Vulnerabilidades críticas — Código corregido
CRÍTICO CORS con allowedOrigins("*") en producción CORS

Permite solicitudes cross-origin desde cualquier dominio, exponiendo endpoints sensibles a ataques CSRF y robo de sesión desde sitios maliciosos.

java — SecurityConfig.java (ANTES)
// ❌ Wildcard — cualquier origen puede llamar la API
config.setAllowedOrigins(List.of("*"));
config.setAllowCredentials(true); // Error: incompatible con *
java — SecurityConfig.java (DESPUÉS)
@Bean
public CorsConfigurationSource corsConfigurationSource() {
  CorsConfiguration config = new CorsConfiguration();
  config.setAllowedOrigins(List.of(
    "https://app.talentflow.io",
    "https://admin.talentflow.io"
  ));
  config.setAllowedMethods(List.of("GET","POST","PUT","DELETE"));
  config.setAllowedHeaders(List.of("Authorization","Content-Type"));
  config.setAllowCredentials(true);
  config.setMaxAge(3600L);
  UrlBasedCorsConfigurationSource source =
      new UrlBasedCorsConfigurationSource();
  source.registerCorsConfiguration("/api/**", config);
  return source;
}
CRÍTICO Secretos hardcodeados en application.yml Secrets

JWT secret y contraseña de BD expuestos en el repositorio. Cualquier acceso al código fuente compromete toda la infraestructura.

yaml — application.yml (ANTES)
# ❌ Nunca en el repo
spring:
  datasource:
    password: Talent2024!DB#Prod
app:
  jwt:
    secret: mySuper$ecretKey123!
    expiration: 86400000
yaml — application.yml (DESPUÉS)
# ✅ Variables de entorno — nunca en git
spring:
  datasource:
    password: ${DB_PASSWORD}
    url: ${DB_URL}
app:
  jwt:
    secret: ${JWT_SECRET}  # min 256-bit
    expiration: ${JWT_EXPIRATION_MS:3600000}
# .env.example (sí en git, sin valores)
# DB_PASSWORD=
# JWT_SECRET=
CRÍTICO SQL Injection en CandidateRepository Injection

Query nativa con concatenación de strings permite inyección SQL. Un atacante puede extraer o modificar toda la base de datos de candidatos.

java — CandidateRepository.java (ANTES)
// ❌ Concatenación = SQL Injection
@Query(value = "SELECT * FROM candidates"
  + " WHERE name = '" + name + "'",
  nativeQuery = true)
List<Candidate> searchByName(String name);
java — CandidateRepository.java (DESPUÉS)
// ✅ Parámetro vinculado — safe
@Query(value = "SELECT * FROM candidates"
  + " WHERE name = :name",
  nativeQuery = true)
List<Candidate> searchByName(
  @Param("name") String name);

// ✅ Mejor aún: Spring Data derivado
List<Candidate> findByNameContainingIgnoreCase(
  String name);
CRÍTICO Sin rate limiting en /api/auth/login — Brute force posible Rate Limit

El endpoint de login no tiene throttling. Un atacante puede hacer miles de intentos por segundo para adivinar contraseñas.

java — RateLimitFilter.java (NUEVO)
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LoginRateLimitFilter extends OncePerRequestFilter {

  private final Map<String, Bucket> loginBuckets = new ConcurrentHashMap<>();

  private Bucket newLoginBucket() {
    return Bucket.builder()
        .addLimit(Bandwidth.classic(5,        // 5 intentos
            Refill.intervally(5, Duration.ofMinutes(15)))) // cada 15 min
        .build();
  }

  @Override
  protected void doFilterInternal(HttpServletRequest req,
      HttpServletResponse res, FilterChain chain)
      throws ServletException, IOException {

    if (req.getMethod().equals("POST")
        && req.getRequestURI().equals("/api/auth/login")) {
      String ip = req.getRemoteAddr();
      Bucket bucket = loginBuckets.computeIfAbsent(ip, k -> newLoginBucket());
      if (!bucket.tryConsume(1)) {
        res.setStatus(429);
        res.setHeader("Retry-After", "900");
        res.getWriter().write("{\"error\":\"Too many login attempts\"}");
        return;
      }
    }
    chain.doFilter(req, res);
  }
}
CRÍTICO DTOs sin validación — XSS e inyección en campos de texto libre Input Validation

Los DTOs de candidato y oferta no tienen anotaciones Bean Validation. Un atacante puede enviar payloads maliciosos que se almacenan y ejecutan en el frontend.

java — CreateCandidateDto.java (CORREGIDO)
// ✅ DTO con validación completa
public record CreateCandidateDto(

    @NotBlank @Size(max = 100)
    @Pattern(regexp = "^[\\p{L}\\s'-]+$", message = "Nombre inválido")
    String name,

    @NotBlank @Email @Size(max = 255)
    String email,

    @Size(max = 2000)
    @NotNull
    String bio,  // sanitizar HTML antes de guardar

    @NotNull @Min(0) @Max(100)
    Integer experienceYears,

    @Valid @NotEmpty
    List<@NotBlank @Size(max=50) String> skills
) {}

// En el controlador:
@PostMapping("/candidates")
@PreAuthorize("hasAnyRole('ADMIN','RECRUITER')")
public ResponseEntity<CandidateDto> create(
    @Valid @RequestBody CreateCandidateDto dto) {
  return ResponseEntity.status(201).body(service.create(dto));
}
CRÍTICO Cabeceras de seguridad HTTP no configuradas Headers

Sin Content-Security-Policy, X-Frame-Options ni HSTS. Expone la app a clickjacking, sniffing y ataques XSS reflejado.

java — SecurityConfig.java — headers completos
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  return http
    .headers(headers -> headers
      .contentSecurityPolicy(csp -> csp.policyDirectives(
          "default-src 'self'; " +
          "script-src 'self'; " +
          "style-src 'self' 'unsafe-inline'; " +
          "img-src 'self' data: https://storage.talentflow.io; " +
          "frame-ancestors 'none'"))
      .frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
      .xssProtection(Customizer.withDefaults())
      .httpStrictTransportSecurity(hsts -> hsts
          .maxAgeInSeconds(31536000).includeSubDomains(true))
      .referrerPolicy(rp -> rp.policy(
          ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN))
    )
    .cors(cors -> cors.configurationSource(corsConfigurationSource()))
    .csrf(csrf -> csrf.disable()) // API stateless con JWT
    .sessionManagement(sm -> sm
      .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    .authorizeHttpRequests(auth -> auth
      .requestMatchers("/api/auth/**", "/actuator/health").permitAll()
      .anyRequest().authenticated()) // deny-by-default
    .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
    .addFilterBefore(loginRateLimitFilter, JwtAuthFilter.class)
    .build();
}
⚠️
Mejoras recomendadas
MEJORA Password encoding con coste apropiado
java — SecurityBeans.java
@Bean
public PasswordEncoder passwordEncoder() {
  return new BCryptPasswordEncoder(12);
  // O Argon2 para mayor resistencia:
  // return new Argon2PasswordEncoder(
  //   16, 32, 1, 65536, 3);
}
MEJORA Dependency scanning en CI/CD
xml — pom.xml (plugin)
<plugin>
  <groupId>org.owasp</groupId>
  <artifactId>dependency-check-maven</artifactId>
  <version>9.2.0</version>
  <configuration>
    <failBuildOnCVSS>7</failBuildOnCVSS>
    <formats>HTML,JSON</formats>
  </configuration>
</plugin>
MEJORA Logging seguro — nunca loguear tokens ni passwords
java (ANTES)
log.debug("Auth request: {}", dto);
// Imprime {email: x, password: secret123}
java (DESPUÉS)
log.debug("Auth attempt for: {}", dto.email());
// Nunca loguear password, token ni datos PII sensibles
// Usar JSON logging estructurado con Logback+ECS
Checklist pre-release — TalentFlow talent-api