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

Hardening : docs, admin, upload, ops

Spring Security
Niveau 28, Leçon 2
Disponible

1. Hardening : objectif et surfaces d’attaque

Quand un projet est déjà « globalement sécurisé », on a très envie de souffler et de dire : « Ça y est, on a des rôles, JWT/session, @PreAuthorize, des tests – la sécurité est faite ». Le hardening est précisément nécessaire à ce moment : il n’ajoute pas de magie, il comble les petites failles par lesquelles les bugs les plus désagréables s’échappent généralement.

Un modèle mental important : l’application a des surfaces (attack surface). Elles ne se résument pas à « tous les endpoints ». Certaines surfaces crient littéralement : « Je suis un outil pratique pour le développeur/opérateur, mais si le monde extérieur me voit, ça va être triste ». Les exemples typiques sont Swagger, Actuator, les endpoints admin et le téléchargement de fichiers.

Ci-dessous, une petite carte des surfaces de notre Secure Content Platform API (pas comme une liste « à faire ainsi », mais comme une façon de regarder l’application) :

Surface Exemples de chemins Pourquoi sensible Idée de base du hardening
Docs / description de l’API /swagger-ui/** C’est la carte de votre API et de ses contrats Fermer explicitement (généralement ADMIN)
Docs / description de l’API /v3/api-docs/** C’est la carte de votre API et de ses contrats Fermer explicitement (généralement ADMIN)
Admin /api/admin/** Dommages élevés si l’accès est ouvert accidentellement Matchers explicites + method security
Upload POST /api/me/avatar Données binaires, taille, type, stockage Limitations + ownership + nuances CSRF
Ops / Actuator /actuator/** Peut fournir des informations sensibles Exposition minimale + règles séparées

Et une autre réflexion importante pour le hardening : vous pouvez être certain de la logique des rôles, mais vous tromper sur les limites. Par exemple, ouvrir Swagger « pour cinq minutes », oublier de le fermer, puis le retrouver deux mois plus tard… mais pas par vous.

Quand le transport ne ment plus sur le schéma de la requête et que l’application comprend son périmètre extérieur, le prochain problème devient très concret : quelles surfaces doivent vraiment être exposées ? Maintenant, réduisons la surface d’attaque et examinons les zones que l’on laisse souvent ouvertes « provisoirement ».

2. Documentation de l’API : pratique, mais pas un endpoint public

Swagger et la documentation OpenAPI sont des outils extrêmement pratiques pour l’équipe de développement. Mais pour un attaquant, c’est aussi très pratique : une carte toute prête des endpoints, des DTO, des exemples de requêtes, et parfois même des indications sur les schémas de sécurité. Donc le hardening des docs n’est pas de la « paranoïa », mais simplement une discipline d’adulte.

Ce que nous protégeons en pratique

En pratique, on retrouve le plus souvent deux ensembles d’URL :

- /swagger-ui/** – page UI (dans le navigateur).

- /v3/api-docs/** – JSON avec le schéma.

Nous n’allons pas nous lancer dans le sujet de la configuration de springdoc et autres bibliothèques (ce n’est pas un cours sur la documentation), mais le principe du hardening ne change pas : si un chemin existe, il doit avoir une règle d’accès explicite.

Mini-exemple : fermons les docs sous ADMIN

Ci-dessous, un petit fragment qui se « colle » logiquement à votre SecurityFilterChain existant. Notez l’idée : les docs ne sont pas « accidentellement fermés parce que .anyRequest().authenticated() », mais fermés explicitement et lisiblement – c’est important pour l’audit.

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
            // Fermons explicitement les docs : pour que l'intention soit visible dans la config,
            // et non pas "apparemment fermé tout seul".
            .requestMatchers("/v3/api-docs/**", "/swagger-ui/**").hasRole("ADMIN")
            // Les autres rè&tildes;gles (API, statique, etc.) sont supposées ci-dessous
            // dans votre configuration réelle.
    );
    return http.build();
}

Cette approche s’accorde bien avec notre idée générale de « surface minimale » : les docs ne sont pas une partie obligatoire du produit pour le monde anonyme, donc nous n’hésitons pas à les restreindre strictement.

Nuance sur CORS et les docs

Parfois on veut « permettre au front de lire le schéma OpenAPI via CORS ». En production, c’est presque toujours un mauvais signe : les endpoints de documentation ne devraient pas être un « API normal pour le navigateur ». Dans une mentalité de hardening, les surfaces opérationnelles/documentation sont des zones séparées, et si vous leur activez CORS, cela doit être une décision très réfléchie, pas un copier-coller de la configuration CORS générale.

3. Zone admin : règles explicites et surface minimale

Les endpoints admin dans un projet sont ces boutons « changer les rôles », « bloquer un compte » et « désactiver un utilisateur ». La différence avec l’API normale est simple : une erreur d’accès ne va pas simplement « afficher un brouillon supplémentaire », mais peut modifier l’état de tout le système. Par conséquent, le hardening de la zone admin est une discipline de « double ceinture de sécurité ».

La première ceinture – les règles au niveau de la requête dans SecurityFilterChain. La deuxième ceinture – la sécurité au niveau de la méthode dans la couche service. Nous ne répéterons pas la sécurité des méthodes (cela a été vu précédemment), mais il est important de se rappeler que le hardening vérifie la cohérence : « le bouton est fermé » et « l’action est fermée » doivent correspondre.

Mini-exemple : porte extérieure par rôle, gestion des utilisateurs par authority

Dans le cours, nous utilisons à la fois des rôles et des authorities. La frontière extérieure de la zone admin peut très bien rester basée sur les rôles (ADMIN), tandis que user:manage est une permission métier plus fine pour les opérations de gestion des utilisateurs à l’intérieur de cette zone. Ainsi, on voit qu’il ne s’agit pas d’un nouveau modèle alternatif pour toute l’admin, mais d’un niveau de précision supplémentaire là où le coût d’une erreur est plus élevé.

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
            // Les endpoints les plus sensibles de gestion des utilisateurs peuvent
            // être fermés par une business authority explicite.
            // On suppose que `user:manage` vit à l'intérieur du modèle admin,
            // et n'est pas attribué à "n'importe qui".
            .requestMatchers("/api/admin/users/**").hasAuthority("user:manage")
            // Le reste de la zone admin reste sous une règle role-based plus large.
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            // Important : les règles "larges" (comme "/api/**") sont généralement
            // placées plus bas pour ne pas écraser celle-ci.
    );
    return http.build();
}

Cela ne signifie pas que le rôle ADMIN est « mauvais ». Simplement, au niveau de la porte extérieure, il sépare la zone admin du reste du monde, tandis qu’une authority comme user:manage rend une action spécifique lisible pour l’audit, les tests et la protection au niveau service.

Nuance de hardening : ordre et matchers « larges »

La zone admin souffre souvent des règles « larges » placées avant dans la configuration. Par exemple, quelqu’un a mis .requestMatchers("/api/**").authenticated() et a ajouté une règle admin en dessous – puis est surpris que l’admin soit accessible à tout utilisateur connecté. Dans la prochaine leçon, nous aurons un dépannage, mais gardez dès maintenant cette pensée de hardening : « les règles larges doivent être en dessous, les règles étroites au-dessus ».

Nuance : manipulations d’URL et pare-feu

Un autre aspect de « surface minimale » concerne les matchers corrects et la protection contre les URL étranges. À l’intérieur de FilterChainProxy, Spring Security utilise HttpFirewall et rejette par défaut les requêtes non normalisées (comme les séquences de path-traversal /../, les doubles //, les paramètres dans le chemin) pour que le pattern-matching fonctionne de manière prévisible.

Le hardening ici est simple : ne commencez pas à « assouplir le pare-feu » sans comprendre la faille que vous ouvrez. Sinon, vous transformez facilement un « simple cas gênant » en contournement des règles.

4. Téléchargement de fichiers : limites, CSRF, stockage

Un fichier n’est pas du JSON. JSON a généralement une taille compréhensible, une structure compréhensible, un validateur et des erreurs standard. Un fichier, en revanche, peut être énorme, étrange, binaire, et il aime très souvent être sauvegardé « n’importe où et n’importe comment ». Donc le téléchargement est un candidat classique au hardening, même si l’accès à l’endpoint est déjà protégé.

Dans notre projet, le téléchargement est POST /api/me/avatar. Il est owner-based (seulement « moi-même »), et semble donc sécurisé. Mais le hardening rappelle : la sécurité du téléchargement ne se limite pas à « qui peut appeler l’endpoint », mais aussi « que peut-on télécharger exactement » et « qu’en faisons-nous ».

Limitation de la taille : limites multipart dans Spring Boot

Si vous n’avez jamais défini de limites, votre application a une fonctionnalité cachée : « n’importe qui peut essayer de remplir votre disque ». Et ce n’est certainement pas une « fonctionnalité » dont il faut être fier lors d’un entretien. Bonne nouvelle : Spring Boot a déjà des limites par défaut pour le téléchargement multipart et permet de les redéfinir facilement.

Selon la documentation de Spring Boot pour Spring MVC, les valeurs par défaut sont : 1 Mo par fichier et 10 Mo pour tous les fichiers d’une même requête, plus un ensemble de propriétés pour les redéfinir et configurer l’emplacement de stockage temporaire. Une position saine de hardening est la suivante : la limite doit être explicite et correspondre au cas métier. Pour un avatar, 2 Mo est généralement plus que suffisant (si votre avatar pèse 40 Mo, ce n’est plus un avatar, c’est une déclaration de position de vie).

Mini-config :

spring:
  servlet:
    multipart:
      # Taille maximale d’un fichier (l’avatar ne doit pas « aspirer » le disque/mémoire du serveur).
      max-file-size: 2MB
      # Taille totale de la requête (dans notre cas, elle égale la taille du fichier).
      max-request-size: 2MB

Ce paramétrage ne résoudra pas tous les problèmes du téléchargement, mais il couvre la classe de risques la plus basique : la taille non contrôlée.

Limitation du type et du nom : whitelist > confiance dans l’extension

Presque chaque débutant tombe un jour dans le piège « je vérifier l’extension du fichier ». Puis la réalité dit : « renomme mon .exe en .png et on verra ce que tu vérifies ». Dans la mentalité de hardening, l’extension est tout au plus un indice, mais pas un critère d’autorisation.

L’approche la plus simple et la plus pédagogique est une whitelist basée sur Content-Type (dans le monde réel, on vérifie aussi la signature du fichier, mais cela dépasse le cadre des fondamentaux).

import java.util.Set;

// Nous n’autorisons que les formats attendus (whitelist), tout le reste est rejeté immédiatement.
Set<String> allowed = Set.of("image/png", "image/jpeg");

// contentType provient des métadonnées multipart (il est important de se rappeler
// qu’il peut être falsifié, mais c’est un bon filtrage de base).
if (!allowed.contains(contentType)) {
    // Le message peut être rendu plus « API-friendly », mais l’idée du hardening
    // est de ne pas tout accepter en vrac.
    throw new IllegalArgumentException("Type d’avatar non pris en charge");
}

Et oui, ce n’est toujours pas une protection idéale (car Content-Type peut être falsifié), mais en tant que base de hardening pour le cours, c’est une étape honnête et compréhensible : « nous n’acceptons pas tout en vrac ».

CSRF + multipart : « l’œuf et la poule » et pourquoi l’en-tête est souvent meilleur

Ensuite commence ce que la documentation de Spring Security appelle le « chicken and egg problem ». Pour se protéger du CSRF, le serveur doit lire le corps de la requête afin d’extraire le token CSRF. Mais lire le corps d’une requête multipart signifie que le fichier a déjà commencé à être téléchargé, et une page externe peut potentiellement « déposer des fichiers temporaires chez vous », même si l’utilisateur n’est pas authentifié.

Spring Security décrit explicitement ce problème et les solutions. Si vous avez un client JavaScript (ce qui est généralement le cas pour une API moderne), la voie recommandée est de transmettre le token CSRF dans un en-tête HTTP pour le multipart. Cela évite de placer le token dans l’URL et de mettre en place des schémas instables.

Si le token est placé dans le corps, alors pour lire le token, le multipart peut être traité avant même que l’authentification soit effectuée, et ainsi « n’importe qui peut placer un fichier temporaire sur le serveur », bien que le traitement de l’application reste accessible uniquement à l’utilisateur authentifié.

Le constat de hardening ici est le suivant : même si vous avez correctement configuré CSRF dans la branche session, le multipart nécessite une attention particulière, et « ça marche » ne signifie pas toujours « c’est sûr et prévisible ».

Stockage : où se trouve le fichier, comment le nommer et quoi écrire en base

La dernière pièce du hardening pour le téléchargement est le stockage. Un anti-patron typique ressemble à ceci : prendre originalFilename et le sauvegarder « tel quel » dans un répertoire que quelqu’un a un jour configuré dans un fichier de configuration. Si on a de la chance, cela casse tout simplement. Si on n’a pas de chance, cela devient une histoire de path traversal et de surprises.

Dans un projet pédagogique, une discipline simple suffit : générer un nom sûr (par exemple, UUID), conserver le nom d’origine uniquement comme métadonnée, et construire le chemin avec Path.resolve plutôt que par concaténation de chaînes.

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

// Génerer un nom sûr : ne pas utiliser originalFilename pour le nom du fichier sur le disque.
Path target = avatarDir.resolve(UUID.randomUUID() + ".bin");

// Copier le flux vers l'emplacement cible.
// Dans le code réel, il est important de traiter supplémentairement :
// fichier déjà existant, droits, atomicité, limites, etc.
Files.copy(multipartFile.getInputStream(), target); // sauvegarde sécurisée

Ensuite, vous associez target au propriétaire (ownerUserId) dans votre modèle StoredFile / UserProfile. Dans notre cours, c’est plus important que « bien sauvegarder une image » : le hardening concerne le contrôle et la prévisibilité du comportement.

5. Actuator : exposition et périmètre séparé

Actuator est un outil très utile : health, info, métriques, état de l’application. Mais il n’est pas seulement utile pour vous. Si vous « accidentellement » ouvrez trop d’endpoints actuator vers l’extérieur, vous pouvez offrir au monde extérieur des informations supplémentaires sur l’application, l’environnement et la configuration. Par conséquent, le hardening commence ici non pas par « comment fermer », mais par « qu’est-ce qu’on ouvre exactement ».

Spring Boot souligne clairement dans sa documentation que les endpoints peuvent contenir des données sensibles et que leur exposition doit être réfléchie. Par défaut, sur HTTP, les chemins sont de la forme /actuator/{id} et health correspond à /actuator/health.

Exposition minimale : qu’ouvrons-nous ?

La première étape du hardening consiste à limiter l’exposition. Dans Boot 4, par défaut sur HTTP et JMX seul health est exposé, et la liste des endpoints exposés est gérée par les propriétés management.endpoints.*.exposure.include/exclude.

Le modèle de hardening ici est simple : si vous n’êtes pas sûr qu’un endpoint est sûr pour le monde extérieur, il ne doit pas être exposé.

Configuration minimale et assez typique pour un projet pédagogique :

management:
  endpoints:
    web:
      exposure:
        # Lister explicitement ce que nous exposons via HTTP (tout le reste n’est pas exposé).
        include: "health,info"

Cela ne signifie pas que info peut toujours être ouvert au public. Cela signifie que vous choisissez un petit ensemble et que vous décidez ensuite de l’accès via les règles de sécurité.

SecurityFilterChain personnalisé et Actuator : pourquoi Boot « se retire »

Une nuance très importante qui casse souvent la tête des gens sur des projets réels : si Spring Security est sur le classpath et qu’il n’y a pas votre propre SecurityFilterChain, alors l’auto-configuration de Boot sait « globalement bien » protéger les endpoints actuator (notamment, ne pas tout laisser ouvert). Mais dès que vous définissez votre propre SecurityFilterChain, l’auto-config de Boot se retire et les règles d’accès aux actuator deviennent entièrement votre responsabilité.

C’est un changement de mentalité dans le hardening : lorsque vous devenez « adulte » et écrivez votre propre chaîne de sécurité, vous prenez automatiquement la responsabilité des endpoints opérationnels. C’est juste : si vous faites une configuration explicite, c’est que vous voulez contrôler le comportement vous-même.

Chaîne séparée pour actuator et danger de « trou » sans chaîne par défaut

Une approche pratique consiste à isoler actuator dans un SecurityFilterChain séparé grâce à securityMatcher, en utilisant EndpointRequest (Spring Boot fournit des matchers prêts pour actuator).

Mais il y a un piège classique : securityMatcher limite la chaîne, et si pour une requête aucune chaîne n’est trouvée, alors la requête n’est pas protégée par Spring Security. La documentation l’avertit explicitement : « If no filter chain matches a particular request, the request is not protected ».

Par conséquent, la règle de hardening est la suivante : en créant une chaîne séparée pour actuator, vous devez impérativement avoir également une chaîne « principale » (default chain) qui couvre les autres requêtes.

Mini-exemple de chaîne séparée pour actuator (je la montre avec httpBasic parce que c’est souvent pratique pour les ops, mais vous pouvez choisir un autre mécanisme s’il existe déjà) :

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
            // Cette chaîne s’applique uniquement aux endpoints actuator (pas à toute l’application).
            .securityMatcher(EndpointRequest.toAnyEndpoint())
            .authorizeHttpRequests(auth -> auth
                    // Minimalement : seulement ADMIN (ou un rôle ops séparé si vous en avez un).
                    .anyRequest().hasRole("ADMIN")
            )
            // Pour les ops, il est souvent pratique d’avoir un mécanisme simple (par exemple,
            // pour Prometheus/healthchecks sous protection).
            .httpBasic(Customizer.withDefaults())
            .build();
}

L’idée clé : les règles actuator ne se mélangent pas avec l’API normale, et vous ne risquez pas d’ouvrir accidentellement /actuator/** via un matcher trop large.

6. Erreurs typiques lors du hardening des docs/admin/upload/actuator

Erreur n°1 : laisser Swagger ouvert « le temps du développement ».
C’est l’un des cas les plus fréquents. Sur le moment, on trouve cela pratique : pour tester à la main, montrer le contrat, vérifier rapidement les DTO. Mais ensuite, « le temps du développement » se transforme en six mois, et les docs deviennent une carte publique de votre API. Le hardening exige que les docs aient une règle d’accès explicite et, idéalement, dépendent du profil, pas de la mémoire de l’équipe.

Erreur n°2 : confondre « exposition des endpoints actuator » et « accès aux endpoints actuator ».
Beaucoup pensent qu’il suffit de management.endpoints.web.exposure.include et que le problème est résolu. Mais cela ne fait que définir « quels endpoints existent sur HTTP ». L’accès à ces endpoints en présence d’un SecurityFilterChain personnalisé est votre responsabilité. Spring Boot explique clairement qu’avec une chaîne personnalisée, l’auto-config se retire et vous définissez les règles.

Erreur n°3 : créer un SecurityFilterChain séparé pour actuator et oublier la chaîne par défaut.
C’est particulièrement insidieux, car on se dit « j’ai tout couvert ». Mais si une chaîne ne matche pas – la requête n’est pas protégée. La documentation le dit clairement : si aucun filter chain ne correspond, la requête n’est pas protégée. Le hardening ici est de toujours garder une chaîne « principale » qui ferme les autres requêtes.

Erreur n°4 : considérer le téléchargement comme sûr parce qu’il est owner-only.
Owner-only protège des autres utilisateurs, mais ne protège pas des « mauvais fichiers ». Un fichier énorme, un type non pris en charge, un nom étrange, un chemin de stockage non sécurisé – tout cela reste un problème même pour le propriétaire. Par conséquent, le hardening du téléchargement inclut des limites et des vérifications de type, et Spring Boot décrit explicitement l’existence de limites par défaut et la possibilité de les redéfinir.

Erreur n°5 : ignorer les nuances CSRF du multipart et tout résoudre avec csrf().disable().
Multipart + CSRF est vraiment un cas particulier. Spring Security décrit le problème « de l’œuf et de la poule » : pour le CSRF, il faut lire le corps, mais lire le corps implique de télécharger le fichier, donc il y a des compromis, et pour les clients JS, le jeton dans l’en-tête est préférable. Le hardening consiste à comprendre le compromis et à choisir une stratégie, pas à désactiver le mécanisme « parce qu’il gêne le téléchargement de l’avatar ».

1
Mission
Spring Security, niveau 28, leçon 2
Bloqué
Hardening docs, admin et actuator
Hardening docs, admin et actuator
1
Mission
Spring Security, niveau 28, leçon 2
Bloqué
Renforcement du téléchargement d'avatar par type, taille et nom de fichier
Renforcement du téléchargement d'avatar par type, taille et nom de fichier
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION