CodeGym /Cursos /Spring Security /Hardening: docs, admin, upload, ops

Hardening: docs, admin, upload, ops

Spring Security
Nivel 28, Lección 2
Disponible

1. Hardening: objetivo y superficies de ataque

Cuando el proyecto ya está «protegido en general», surge un deseo muy humano de relajarse y decir: «Bueno, ya tenemos roles, JWT/sesión, @PreAuthorize, pruebas — la seguridad está hecha». El hardening es necesario precisamente en este momento: no añade magia, elimina pequeñas grietas por las que suelen filtrarse los errores más desagradables.

Modelo mental importante: la aplicación tiene superficies (attack surface). No son equivalentes a «todos los endpoints». Algunas superficies gritan directamente: «Soy una cosa conveniente para el desarrollador/operador, pero si el mundo exterior me ve, será triste». Ejemplos típicos: Swagger, Actuator, endpoints de administración y subida de archivos.

A continuación, un pequeño mapa de superficies de nuestra Secure Content Platform API (no como una lista de «haz esto», sino como una forma de mirar la aplicación):

Superficie Ejemplos de rutas Por qué es sensible Idea básica de hardening
Documentación / Descripción de API /swagger-ui/** Es el mapa de su API y contratos Cerramos explícitamente (generalmente ADMIN)
Documentación / Descripción de API /v3/api-docs/** Es el mapa de su API y contratos Cerramos explícitamente (generalmente ADMIN)
Administración /api/admin/** Alto daño si el acceso se abre accidentalmente Matchers explícitos + seguridad a nivel de método
Subida de archivos POST /api/me/avatar Datos binarios, tamaño, tipo, almacenamiento Restricciones + ownership + matices de CSRF
Operaciones / Actuator /actuator/** Puede proporcionar información sensible Exposición mínima + reglas separadas

Y otra idea importante para el hardening: puedes estar seguro de la lógica de roles, pero equivocarte en los límites. Por ejemplo, abrir Swagger «por cinco minutos», olvidar cerrarlo, y luego encontrarlo dos meses después… pero ya no eres tú.

Cuando el transporte ya no miente sobre el esquema de la solicitud y la aplicación entiende su contorno externo, el siguiente problema se vuelve muy terrenal: qué superficies deberían sobresalir hacia afuera. Ahora reduciremos la superficie de ataque y veremos las zonas que con mayor frecuencia se dejan abiertas «temporalmente».

2. Documentación de la API: conveniente, pero no un endpoint público

La documentación de Swagger y OpenAPI — una cosa increíblemente conveniente… para el equipo de desarrollo. Pero para un atacante también es una cosa conveniente: un mapa listo de endpoints, DTOs, ejemplos de solicitudes, a veces incluso pistas sobre esquemas de seguridad. Por lo tanto, el hardening de la documentación no es «paranoia», sino simplemente una disciplina madura.

¿Qué protegemos en la práctica?

En la práctica, suelen aparecer dos conjuntos de URL:

- /swagger-ui/** — página de UI (en el navegador).

- /v3/api-docs/** — JSON con el esquema.

No nos adentraremos en el tema de configurar springdoc y otras bibliotecas (este no es un curso sobre documentación), pero el principio de hardening no cambia: si la ruta existe, debe tener una regla de acceso explícita.

Mini-ejemplo: cerramos la documentación bajo ADMIN

A continuación, un pequeño fragmento que lógicamente se «pega» a su SecurityFilterChain existente. Preste atención a la idea: la documentación no se cerró «porque .anyRequest().authenticated()», sino que se cerró explícita y legiblemente — esto es importante para la auditoría.

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
            // Cerramos explícitamente la documentación: para que en la configuración se vea la intención, no «como si se hubiera cerrado solo».
            .requestMatchers("/v3/api-docs/**", "/swagger-ui/**").hasRole("ADMIN")
            // Las demás reglas (API, estática, etc.) se asumen a continuación en su configuración real.
    );
    return http.build();
}

Este enfoque se combina bien con nuestra idea general de «superficie mínima»: la documentación no es una parte obligatoria del producto para el mundo anónimo, por lo que no dudamos en cerrarla estrictamente.

Matiz sobre CORS y documentación

A veces se quiere «permitir que el frontend lea el esquema OpenAPI por CORS». En producción, esto casi siempre es una mala señal: los endpoints de documentación no deberían ser una «API normal para el navegador». En el pensamiento de hardening, las superficies operacionales/de documentación son zonas separadas, y si de repente habilita CORS para ellas, debe ser una decisión muy consciente, no un copia y pega de la configuración general de CORS.

3. Zona de administración: reglas explícitas y superficie mínima

Los endpoints de administración en el proyecto son esos botones de «cambiar roles», «bloquear cuenta» y «deshabilitar usuario». La diferencia con una API normal es simple: un error en el acceso no solo «mostrará un borrador extra», sino que puede cambiar el estado de todo el sistema. Por lo tanto, el hardening de la zona de administración es una disciplina de «doble cinturón de seguridad».

El primer cinturón son las reglas a nivel de solicitud en SecurityFilterChain. El segundo cinturón es la seguridad a nivel de método en la capa de servicio. No repetiremos la seguridad a nivel de método (ya se cubrió antes), pero es importante recordar que el hardening es una verificación de consistencia: «el botón está cerrado» y «la acción está cerrada» deben coincidir.

Mini-ejemplo: puerta exterior por rol, gestión de usuarios por autoridad

En el curso utilizamos roles y autoridades. El límite exterior de la zona de administración puede muy bien seguir siendo basado en roles (ADMIN), y user:manage es un permiso de negocio más estrecho para las operaciones de gestión de usuarios dentro de ella. Así se ve que no es un nuevo modelo alternativo de toda la administración, sino el siguiente grado de precisión donde el costo del error es mayor.

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
            // Los endpoints de gestión de usuarios más sensibles se pueden cerrar con una autoridad de negocio explícita.
            // Suponemos que `user:manage` vive dentro del modelo de administración, no se asigna a «cualquiera».
            .requestMatchers("/api/admin/users/**").hasAuthority("user:manage")
            // El resto de la zona de administración se rige por una regla basada en roles más amplia.
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            // Importante: las reglas «amplias» (como "/api/**") suelen colocarse más abajo para no sobrescribir esto.
    );
    return http.build();
}

Esto no significa que el rol ADMIN sea «malo». Simplemente, a nivel de puerta exterior separa la zona de administración del resto del mundo, y una autoridad como user:manage hace que la acción concreta sea legible para auditoría, pruebas y protección a nivel de servicio.

Matiz de hardening: orden y matchers «amplios»

La zona de administración a menudo sufre de reglas «amplias» más arriba en la configuración. Por ejemplo, alguien puso .requestMatchers("/api/**").authenticated() y luego agregó una regla de administración abajo, y luego se pregunta por qué la administración se volvió accesible para cualquier usuario conectado. En la próxima lección tendremos solución de problemas, pero desde ahora tenga en mente la idea de hardening: «las reglas amplias deben estar abajo, las estrechas arriba».

Matiz: manipulación de URL y firewall

Otro aspecto de «superficie mínima» son los matchers correctos y la protección contra URL extrañas. Dentro de FilterChainProxy, Spring Security usa HttpFirewall y por defecto rechaza solicitudes no normalizadas (como secuencias de path-traversal /../, duplicados //, parámetros en la ruta) para que el pattern-matching funcione de manera predecible.

El hardening aquí es simple: no empiece a «debilitar el firewall» hasta que entienda qué agujero está abriendo. De lo contrario, fácilmente convierte «un caso simplemente incómodo» en una elusión de reglas.

4. Subida de archivos: límites, CSRF, almacenamiento

Un archivo no es JSON. JSON normalmente tiene un tamaño comprensible, estructura clara, validador y errores estándar. En cambio, un archivo puede ser enorme, extraño, binario, y además le encanta que lo guarden «en cualquier lugar y de cualquier manera». Por lo tanto, la subida de archivos es un candidato clásico para hardening, incluso si el acceso al endpoint ya está protegido.

En nuestro proyecto, la subida es POST /api/me/avatar. Está basado en el propietario (solo «yo mismo»), y por eso parece seguro. Pero el hardening recuerda: la seguridad de la subida no es solo «quién puede llamar al endpoint», sino también «qué se puede cargar» y «qué hacemos con ello».

Limitación de tamaño: límites de multipart en Spring Boot

Si nunca ha establecido límites, su aplicación tiene una característica oculta: «cualquiera puede intentar llenar su disco». Y esa no es una «característica» de la que deba estar orgulloso en una entrevista. La buena noticia: Spring Boot ya tiene por defecto límites en la subida multipart y permite anularlos convenientemente.

Según la documentación de Spring Boot para Spring MVC, hay valores por defecto: 1MB por archivo y 10MB para todos los archivos en una sola solicitud, además de un conjunto de propiedades para anular y configurar la ubicación de almacenamiento temporal. La posición saludable de hardening es: el límite debe ser explícito y corresponder al caso de negocio. Para un avatar, 2MB suele ser más que suficiente (si su avatar pesa 40MB, ya no es un avatar, es una declaración de posición de vida).

Mini-config:

spring:
  servlet:
    multipart:
      # Tamaño máximo de un solo archivo (el avatar no debe «aspirar» el disco/memoria del servidor).
      max-file-size: 2MB
      # Tamaño total de la solicitud (en nuestro caso, es igual al tamaño del archivo).
      max-request-size: 2MB

Limitación de tipo y nombre: lista blanca > confianza en la extensión

Casi todos los principiantes alguna vez caen en la trampa de «verificaré la extensión del archivo». Luego la realidad dice: «renómbrame .exe a .png y veré qué estás revisando». En el pensamiento de hardening, la extensión es como máximo una pista, pero no un criterio de admisión.

El enfoque más simple y didácticamente comprensible es una lista blanca por Content-Type (y en el mundo real también se verifica la firma del archivo, pero eso ya está fuera de los fundamentos).

import java.util.Set;

// Permitimos solo los formatos esperados (lista blanca), todo lo demás se rechaza inmediatamente.
Set<String> allowed = Set.of("image/png", "image/jpeg");

// contentType se obtiene de los metadatos multipart (importante recordar: se puede falsificar, pero es un buen filtrado básico).
if (!allowed.contains(contentType)) {
    // El mensaje se puede hacer más «amigable para la API», pero la idea misma del hardening es no aceptar cualquier cosa.
    throw new IllegalArgumentException("Unsupported avatar type");
}

Y sí, sigue sin ser una protección ideal (porque Content-Type se puede falsificar), pero como base de hardening para el curso, es un paso honesto y comprensible: «no aceptamos cualquier cosa».

CSRF + multipart: «el huevo y la gallina» y por qué el header suele ser mejor

Luego comienza lo que en la documentación de Spring Security llaman «problema del huevo y la gallina». Para protegerse contra CSRF, el servidor necesita leer el cuerpo de la solicitud para obtener el token CSRF. Pero leer el cuerpo de una solicitud multipart significa que el archivo ya ha comenzado a cargarse, y una página externa potencialmente puede «llenarle archivos temporales», incluso si el usuario no está autenticado.

Spring Security describe directamente este problema y las opciones. Si tiene un cliente JavaScript (y para una API moderna suele ser así), el camino recomendado es enviar el token CSRF en el encabezado HTTP para multipart. Esto permite no poner el token en la URL ni crear esquemas inestables.

Si el token se coloca en el cuerpo, para leer el token, el multipart puede procesarse antes de que se ejecute la autorización, y entonces «cualquiera puede colocar un archivo temporal en el servidor», aunque el procesamiento de la aplicación seguirá estando disponible solo para usuarios autenticados.

La conclusión de hardening aquí es: incluso si ya ha configurado correctamente CSRF en la rama de sesión, multipart requiere atención por separado, y «simplemente funciona» no siempre significa «seguro y predecible».

Almacenamiento: dónde está el archivo, cómo nombrarlo y qué escribir en la BD

La última pieza de hardening para la subida es el almacenamiento. Un antipatrón típico es: tomar originalFilename y guardarlo «tal cual» en un directorio que alguien configuró alguna vez en el archivo de configuración. Si tienes suerte, simplemente se romperá. Si no tienes suerte, se convertirá en una historia sobre path traversal y sorpresas.

En el proyecto de aprendizaje es suficiente una disciplina simple: generar un nombre seguro (por ejemplo, UUID), guardar el nombre original solo como metadato, y construir la ruta mediante Path.resolve, no mediante concatenación de cadenas.

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;

// Generamos un nombre seguro: no usamos originalFilename para el nombre del archivo en disco.
Path target = avatarDir.resolve(UUID.randomUUID() + ".bin");

// Copiamos el flujo al destino.
// En código real es importante manejar adicionalmente: archivo existente, permisos, atomicidad, límites, etc.
Files.copy(multipartFile.getInputStream(), target); // guardamos de forma segura

Y luego vincula target con el propietario (ownerUserId) en su modelo StoredFile / UserProfile. En nuestro curso, esto es más importante que «guardar la imagen bonita»: el hardening se trata de control y previsibilidad del comportamiento.

5. Actuator: exposición y contorno separado

Actuator es algo muy útil: health, info, métricas, estado de la aplicación. Pero no es útil solo para usted. Si «accidentalmente» abre demasiados endpoints de actuator hacia afuera, puede regalar al mundo exterior información extra sobre la aplicación, el entorno y la configuración. Por lo tanto, el hardening aquí no comienza con «cómo cerrar», sino con «qué abrimos en absoluto».

Spring Boot en la documentación subraya directamente que los endpoints pueden contener datos sensibles y su exposición debe ser pensada, y por defecto sobre HTTP se muestra la ruta /actuator/{id} y health corresponde a /actuator/health.

Exposición mínima: qué abrimos en absoluto

El primer paso del hardening es limitar la exposición. En Boot 4, por defecto, solo se expone health por HTTP y JMX, y la lista de endpoints expuestos se gestiona mediante las propiedades management.endpoints.*.exposure.include/exclude.

El modelo de hardening aquí es simple: si no está seguro de que el endpoint sea seguro para el mundo exterior, no debe estar expuesto.

Un config mínimo y bastante típico para un proyecto de aprendizaje:

management:
  endpoints:
    web:
      exposure:
        # Listamos explícitamente lo que entregamos por HTTP (todo lo demás no se expone).
        include: "health,info"

Esto no significa que info siempre se pueda abrir públicamente. Significa que elige un conjunto pequeño y luego decide el acceso a través de reglas de seguridad.

Custom SecurityFilterChain y Actuator: por qué Boot «se echa atrás»

Un matiz muy importante que a menudo confunde a las personas en proyectos reales: si Spring Security está en el classpath y no tiene su propio SecurityFilterChain, entonces la auto-configuración de Boot puede «en general correctamente» cubrir los endpoints de actuator (en particular, no dejar todo abierto). Pero tan pronto como define su propio SecurityFilterChain, la auto-configuración de Boot se retira, y las reglas de acceso a actuator son completamente su responsabilidad.

Este es un giro de pensamiento de hardening: cuando se vuelve «adulto» y escribe su propio security chain, automáticamente asume la responsabilidad también por los endpoints operativos. Es justo: si hace una configuración explícita, significa que quiere controlar el comportamiento usted mismo.

Cadena separada para actuator y peligro de «agujero» sin cadena por defecto

Un enfoque práctico y conveniente es separar actuator en un SecurityFilterChain independiente mediante securityMatcher, usando EndpointRequest (Spring Boot proporciona matchers listos para actuator).

Pero aquí hay una trampa clásica: securityMatcher limita la cadena, y si para alguna solicitud no se encuentra una cadena, entonces la solicitud no está protegida por Spring Security en absoluto. La documentación advierte directamente sobre esto: «If no filter chain matches a particular request, the request is not protected».

Por lo tanto, la regla de hardening es: al hacer una cadena separada para actuator, debe tener también una cadena «principal» (cadena por defecto) que cubra las demás solicitudes.

Mini-ejemplo de cadena separada para actuator (lo muestro con httpBasic, porque a menudo es conveniente para operaciones, pero puede elegir otro mecanismo si ya lo tiene):

import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.web.SecurityFilterChain;

@Order(1)
@Bean
SecurityFilterChain actuatorChain(org.springframework.security.config.annotation.web.builders.HttpSecurity http)
        throws Exception {
    return http
            // Esta cadena se aplica solo a los endpoints de actuator (no a toda la aplicación).
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                    // Mínimo: solo ADMIN (o un rol de operaciones separado, si lo tiene).
                    .anyRequest().hasRole("ADMIN")
            )
            // Para operaciones, a menudo es conveniente tener un mecanismo simple (por ejemplo, para Prometheus/healthchecks bajo protección).
            .httpBasic(Customizer.withDefaults())
            .build();
}

6. Errores típicos al hacer hardening de documentación/administración/subida/actuator

Error n.° 1: dejar Swagger abierto «durante el desarrollo».
Este es uno de los casos más frecuentes. En el momento parece conveniente: probar manualmente, mostrar el contrato, verificar rápidamente los DTO. Pero luego el «tiempo de desarrollo» se convierte en seis meses, y la documentación se convierte en un mapa público de su API. El hardening requiere que la documentación tenga una regla de acceso explícita y, preferiblemente, dependa del perfil, no de la memoria del equipo.

Error n.° 2: confundir «exposición de endpoints de actuator» y «acceso a endpoints de actuator».
Muchos piensan que basta con management.endpoints.web.exposure.include y el problema está resuelto. Pero eso solo define «qué endpoints existen por HTTP». El acceso a ellos en presencia de un SecurityFilterChain personalizado es su responsabilidad. Spring Boot describe explícitamente que con una cadena personalizada, la auto-configuración se retira y usted establece las reglas.

Error n.° 3: crear un SecurityFilterChain separado para actuator y olvidarse de la cadena por defecto.
Esto es especialmente engañoso porque en la mente parece: «ya cubrí todo». Pero si alguna cadena no coincide, la solicitud no está protegida. La documentación dice directamente: si ninguna filter chain coincide, la solicitud no estará protegida. El hardening aquí es mantener siempre una cadena «principal» que cubra las demás solicitudes.

Error n.° 4: considerar la subida segura porque es solo para el propietario.
Solo para el propietario protege contra usuarios externos, pero no protege contra «archivos malos». Un archivo enorme, un tipo no soportado, un nombre extraño, una ruta de almacenamiento insegura: todo eso sigue siendo un problema incluso para el propietario. Por lo tanto, el hardening de la subida incluye límites y verificaciones de tipo, y Spring Boot describe directamente la existencia de límites predeterminados y la posibilidad de anularlos.

Error n.° 5: ignorar los matices de CSRF en multipart y arreglarlo todo con csrf().disable().
Multipart + CSRF es realmente un caso especial. Spring Security describe el problema del «huevo y la gallina»: para CSRF es necesario leer el cuerpo, pero leer el cuerpo significa cargar el archivo, por lo tanto hay compromisos, y para clientes JS es preferible el token en el encabezado. El hardening consiste en entender el compromiso y elegir una estrategia, no en desactivar el mecanismo «porque interfiere con la carga del avatar».

1
Tarea
Spring Security, nivel 28, lección 2
Bloqueada
Hardening docs, admin y actuator
Hardening docs, admin y actuator
1
Tarea
Spring Security, nivel 28, lección 2
Bloqueada
Hardening de carga de avatar por tipo, tamaño y nombre de archivo
Hardening de carga de avatar por tipo, tamaño y nombre de archivo
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION