GHSA-6c8g-9hfh-pq5hCritical
HAXcms: Private Key Disclosure via Broken HMAC Implementation
🔗 CVE IDs covered (1)
📋 Description
### Summary
The `hmacBase64()` function in the HAXcms Node.js backend contains two critical cryptographic implementation errors that together allow any unauthenticated attacker to extract the system’s private signing key and forge arbitrary admin-level JSON Web Tokens (JWTs) allowing them to get full admin access with a single HTTP request.
### Details
Bug 1: Hardcoded HMAC Key (line 2160): The function passes the literal string "0" as the HMAC signing key instead of the key parameter, making every HAXcms instance compute identical HMACs for the same input.
Bug 2: Private Key Appended to Output (lines 2161- 2163): After computing the HMAC, the function concatenates the real key parameter which is "this.privateKey + this.salt", the system’s master signing secret is directly onto the output. The combined buffer is base64-encoded and returned as the token.
Every base64url token produced has the same structure: 32 bytes HMAC keyed with "0" and N bytes of `privateKey+salt`. An attacker base64-decodes any token, discards the first 32 bytes, and reads the private key directly.
The `/system/api/connectionSettings` endpoint is unauthenticated and returns multiple tokens generated by this function. A single GET request to this endpoint exposes the private key.
The PHP backend (HAXCMS.php:1619-1631) implements this function correctly with the actual key and returns only the hash. The PHP version produces 44-character tokens whereas the broken Node.js version produces 139+ character tokens.
### PoC
1. GET request to `/system/api/connectionSettings` endpoint and fetch the token.
2. Extract the private key from the fetched token. The `hmacBase64()` function produces 32 bytes with HMAC-SHA256 with hardcoded key "0" and the rest of the bytes are `privateKey+salt` (plaintext). Decode the Base64 token, discard the first 32 bytes, read the remaining bytes as UTF-8 (this is your extracted private key).
3. Since JWT's are signed with `privateKey+salt`, use this stolen private key to forge a JWT for admin using `JWT.sign(payload, this.privateKey+this.salt)`. NOTE: the payload uses {id, user (set this as admin), iat (current timestamp), exp (expiration timestamp)}
4. The same key can also be used to create other tokens (user_token, base_token, form_token, etc).
5. Use these forged tokens to hit all authenticated endpoints (modify/delete/create etc) with admin privileges.
### Impact
An unauthenticated attacker can perform the complete attack chain with a single HTTP request:
1. Extract private key: GET "/system/api/connectionSettings", base64-decode any token, discard first 32 bytes.
2. Forge admin JWT: sign arbitrary JWT payloads with the stolen privateKey+salt.
3. Forge all request tokens: compute valid user_token, site_token for any API call.
4. Full admin access: create/modify/delete sites, upload files, modify content.
This works even if the admin has changed the default credentials to a strong password. The forged tokens produce no login events in logs.
🎯 Affected products1
- npm/@haxtheweb/haxcms-nodejs:<= 25.0.0