CVE-2026-46395

HAXcms: Private Key Disclosure via Broken HMAC Implementation

Published: 5/19/2026Modified: 5/19/2026
Also known as:GHSA-6c8g-9hfh-pq5h

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 packages (1)

CVSS scores

SourceVersionSeverityVector
osvCVSS 4.0CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

References (2)