HAX CMS NodeJs's Disabled Content Security Policy Enables Cross-Site Scripting
Description
HAX CMS NodeJs allows users to manage their microsite universe with a NodeJs backend. In versions 11.0.7 and below, the NodeJS version of HAX CMS has a disabled Content Security Policy (CSP). This configuration is insecure for a production application because it does not protect against cross-site-scripting attacks. The contentSecurityPolicy value is explicitly disabled in the application's Helmet configuration in app.js. This is fixed in version 11.0.8.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
HAX CMS Node.js versions ≤11.0.7 disable Helmet's Content Security Policy, leaving the application unprotected against cross-site-scripting attacks.
Root
Cause
The Node.js version of HAX CMS (versions 11.0.7 and below) explicitly disabled Content Security Policy (CSP) in its Helmet middleware configuration. In app.js, the Helmet initialization was called with contentSecurityPolicy: false [1][4]. This configuration instructs the application to omit CSP headers from HTTP responses, removing a key defense-in-depth layer against cross-site-scripting (XSS) attacks.
Attack
Vector
An attacker who can inject malicious scripts into the application (e.g., through a stored or reflected XSS vulnerability) would not be blocked by CSP. The absence of CSP directives means the browser will execute any inline scripts or load external resources without restriction [2]. The vulnerability impacts all installations running affected versions, regardless of deployment profile [3]. Exploitation requires the attacker to first leverage an independent XSS flaw, but the disabled CSP significantly lowers the bar for successful script execution and data exfiltration [4].
Impact
With CSP disabled, an attacker can execute arbitrary scripts in the context of a victim's session. This enables theft of session tokens, access to localStorage/sessionStorage data, and other sensitive information accessible through the browser's origin [4]. The OWASP guidance, referenced in the advisory, underscores that CSP is a critical mitigation for modern web applications [4].
Mitigation
The issue is fixed in version 11.0.8 of HAX CMS Node.js. The patch, visible in commit ddb9351, replaces the disabled CSP with a properly configured set of directives that restrict script sources, style sources, and other resource-loading policies while preserving compatibility with necessary external services like YouTube and Google Fonts [2]. Users are strongly advised to upgrade to version 11.0.8 or later.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@haxtheweb/haxcms-nodejsnpm | < 11.0.8 | 11.0.8 |
Affected products
2- haxtheweb/issuesv5Range: < 11.0.8
Patches
1ddb9351c6d64https://github.com/haxtheweb/issues/security/advisories/GHSA-59g8-h59f-8hjp and additional clean up related to https://github.com/haxtheweb/issues/security/advisories/GHSA-9jr9-8ff3-m894
10 files changed · +33 −219
nodemon.json+2 −1 modified@@ -8,5 +8,6 @@ "_sites", "_sites/*/**", "_sites/*/pages/**", - "_sites/*/site.json"] + "_sites/*/site.json" + ] } \ No newline at end of file
src/app.js+26 −9 modified@@ -15,10 +15,35 @@ const server = require('http').Server(app); // HAXcms core settings process.env.haxcms_middleware = "node-express"; const { HAXCMS, systemStructureContext } = require('./lib/HAXCMS.js'); +// default helmet policies for CSP +const helmetPolicies = { + contentSecurityPolicy: { + directives: { + scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "'wasm-unsafe-eval'", "www.youtube.com"], + styleSrc: ["'self'", "'unsafe-inline'", "data:", "https:"], + mediaSrc: ["'self'", "data:", "https:"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'", "https:", "ws:"], + defaultSrc: ["'self'", "data:", "https:"], + objectSrc: ["'none'"], + fontSrc: ["'self'", "data:", "fonts.gstatic.com"], + }, + }, +// crossOriginResourcePolicy: false, +// crossOriginEmbedderPolicy: 'require-corp', +// crossOriginOpenerPolicy: 'same-origin', + referrerPolicy: { + policy: ["origin", "unsafe-url"], + }, +}; + // flag in local development that disables security // this way you launch from local and don't need a U/P relationship if (process.env.HAXCMS_DISABLE_JWT_CHECKS || argv._.includes('HAXCMS_DISABLE_JWT_CHECKS')) { HAXCMS.HAXCMS_DISABLE_JWT_CHECKS = true; + // disable security policies that would otherwise block local development + // also enables webcontainer environments which is what our playground runs + helmetPolicies.contentSecurityPolicy = false; } // routes with all requires const { RoutesMap, OpenRoutes } = require('./lib/RoutesMap.js'); @@ -48,15 +73,7 @@ if (process.env.NODE_ENV === "development") { ); } app.use(express.urlencoded({limit: '50mb', extended: false, parameterLimit: 50000 })); -app.use(helmet({ - contentSecurityPolicy: false, -// crossOriginResourcePolicy: false, -// crossOriginEmbedderPolicy: 'require-corp', -// crossOriginOpenerPolicy: 'same-origin', - referrerPolicy: { - policy: ["origin", "unsafe-url"], - }, -})); +app.use(helmet(helmetPolicies)); app.use(cookieParser()); //pre-flight requests app.options('*', function(req, res, next) {
src/lib/HAXCMS.js+2 −85 modified@@ -1911,7 +1911,8 @@ class HAXCMSClass { } cleanTitle = cleanTitle.replace(/ /g, '-').toLowerCase(); cleanTitle = cleanTitle.replace('/[^\w\-\/\s]+/u', '-'); - cleanTitle = cleanTitle.replace('/--+/u', '-'); + cleanTitle = cleanTitle.replace('/--+/u', + '-'); // ensure we don't return an empty title or it could break downstream things if (cleanTitle == '') { cleanTitle = 'blank'; @@ -2339,90 +2340,6 @@ class HAXCMSClass { } this.saveUserDataFile(); } - /** - * Set and validate config - */ - async setConfig(values) - { - if ((values.apis)) { - for (var key in values.apis) { - let val = values.apis[key]; - this.config.appStore.apiKeys[key] = val; - } - } - if (!(this.config.site)) { - this.config.site = {}; - } - if (!(this.config.site.git)) { - this.config.site.git = {}; - } - if (values.publishing) { - for (var key in values.publishing) { - let val = values.publishing[key]; - this.config.site.git[key] = val; - } - } - // test for a password in order to do the git hook up this one time - if ( - (this.config.site.git.email) && - (this.config.site.git.pass) - ) { - email = this.config.site.git.email; - pass = this.config.site.git.pass; - // ensure we never save the password, this is just a 1 time pass through - delete this.config.site.git.pass; - } - // save config to the file - this.saveConfigFile(); - // see if we need to set a github key for publishing - // this is a one time thing that helps with the workflow - if ( - (email) && - (pass) && - !(this.config.site.git.keySet) && - (this.config.site.git.vendor) && - this.config.site.git.vendor == 'github' - ) { - let json = {}; - json.title = 'HAXCMS Publishing key'; - json.key = this.getSSHKey(); - let response = fetch('https://api.github.com/user/keys', - { - method: "POST", - body: { - 'auth': [email, pass], - 'body': JSON.stringify(json) - }, - } - ); - // we did it, now store that it worked so we can skip all this setup in the future - if (response.getStatusCode() == 201) { - this.config.site.git.keySet = true; - this.saveConfigFile(); - try { - // set global config for username / email if we can - const gitRepo = new GitPlus({ - dir: this.siteDirectory, - cliVersion: await this.gitTest() - }); - gitRepo.gitExec( - 'config --global user.name "' + - this.config.site.git.user + - '"' - ); - gitRepo.gitExec( - 'config --global user.email "' + - this.config.site.git.email + - '"' - ); - } - catch(e){} - } - - return response.getStatusCode(); - } - return 'saved'; - } /** * Write configuration to the config file */
src/lib/RoutesMap.js+0 −4 modified@@ -21,10 +21,6 @@ const RoutesMap = { saveNode: require('../routes/saveNode.js'), deleteNode: require('../routes/deleteNode.js'), saveFile: require('../routes/saveFile.js'), - - getConfig: require('../routes/getConfig.js'), - setConfig: require('../routes/setConfig.js'), - getNodeFields: require('../routes/getNodeFields.js'), }, get: { logout: require('../routes/logout.js'),
src/routes/connectionSettings.js+0 −3 modified@@ -47,9 +47,6 @@ async function connectionSettings(req, res) { saveNodePath: `${baseAPIPath}saveNode`, saveManifestPath: `${baseAPIPath}saveManifest`, saveOutlinePath: `${baseAPIPath}saveOutline`, - setConfigPath:`${baseAPIPath}setConfig`, - getConfigPath: `${baseAPIPath}getConfig`, - getNodeFieldsPath: `${baseAPIPath}getNodeFields`, getSiteFieldsPath: `${baseAPIPath}formLoad?haxcms_form_id=siteSettings`, createNodePath: `${baseAPIPath}createNode`, getUserDataPath: `${baseAPIPath}getUserData`,
src/routes/createSite.js+2 −2 modified@@ -52,7 +52,7 @@ const HAXCMSFile = require('../lib/HAXCMSFile.js'); * ) */ async function createSite(req, res) { - if (HAXCMS.validateRequestToken()) { + if (HAXCMS.validateRequestToken(req.body.token)) { let domain = null; // woohoo we can edit this thing! if (req.body['site']['domain'] && req.body['site']['domain'] != null && req.body['site']['domain'] != '') { @@ -216,7 +216,7 @@ async function createSite(req, res) { }); } else { - res.send(403); + res.sendStatus(403); } } module.exports = createSite; \ No newline at end of file
src/routes/formLoad.js+1 −1 modified@@ -18,7 +18,7 @@ const { HAXCMS } = require('../lib/HAXCMS.js'); * ) */ async function formLoad(req, res) { - if (HAXCMS.validateRequestToken(null, 'form')) { + if (HAXCMS.validateRequestToken(req.body.token, 'form')) { let context = { 'site':[], 'node': [],
src/routes/getConfig.js+0 −32 removed@@ -1,32 +0,0 @@ -const { HAXCMS } = require('../lib/HAXCMS.js'); - -/** - * @OA\Post( - * path="/getConfig", - * tags={"cms","authenticated","settings"}, - * @OA\Parameter( - * name="jwt", - * description="JSON Web token, obtain by using /login", - * in="query", - * required=true, - * @OA\Schema(type="string") - * ), - * @OA\Response( - * response="200", - * description="Get configuration for HAXcms itself" - * ) - * ) - */ - function getConfig(req, res) { - response = {}; - response.schema = HAXCMS.getConfigSchema(); - response.values = HAXCMS.config; - for(var key in response.values.appStore) { - let val = response.values.appStore[key]; - if (key !== 'apiKeys') { - delete (response.values.appStore[key]); - } - } - retres.send(response); - } - module.exports = getConfig; \ No newline at end of file
src/routes/getNodeFields.js+0 −31 removed@@ -1,31 +0,0 @@ -const { HAXCMS } = require('../lib/HAXCMS.js'); - -/** - * @OA\Post( - * path="/getNodeFields", - * tags={"cms","authenticated","node","form"}, - * @OA\Parameter( - * name="jwt", - * description="JSON Web token, obtain by using /login", - * in="query", - * required=true, - * @OA\Schema(type="string") - * ), - * @OA\Response( - * response="200", - * description="Update the alternative formats surrounding a site" - * ) - * ) - */ - async function getNodeFields(req, res) { - if (HAXCMS.validateRequestToken(null, 'form')) { - let site = await HAXCMS.loadSite(req.body['site']['name']); - if (page = site.loadNode(req.body['node']['id'])) { - schema = site.loadNodeFieldSchema(page); - res.send(schema); - } - } else { - res.send(403); - } - } - module.exports = getNodeFields; \ No newline at end of file
src/routes/setConfig.js+0 −51 removed@@ -1,51 +0,0 @@ -const { HAXCMS } = require('../lib/HAXCMS.js'); - -/** - * @OA\Post( - * path="/setConfig", - * tags={"cms","authenticated","form","settings"}, - * @OA\Parameter( - * name="jwt", - * description="JSON Web token, obtain by using /login", - * in="query", - * required=true, - * @OA\Schema(type="string") - * ), - * @OA\RequestBody( - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * @OA\Property( - * property="values", - * type="object" - * ), - * required={"site"}, - * example={ - * "values": {} - * } - * ) - * ) - * ), - * @OA\Response( - * response="200", - * description="Set configuration for HAXcms" - * ) - * ) - */ - async function setConfig(req, res) { - if (HAXCMS.validateRequestToken()) { - values = req['body']['values']; - val = {}; - if ((values.apis) && (values.appStore.apiKeys)) { - val.apis = values.apis; - } - if ((values.publishing)) { - val.publishing = values.publishing; - } - response = await HAXCMS.setConfig(val); - res.send(response); - } else { - res.send(403); - } - } - module.exports = setConfig; \ No newline at end of file
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-59g8-h59f-8hjpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-54128ghsaADVISORY
- github.com/haxtheweb/haxcms-nodejs/commit/ddb9351c6d6418008d4084a5b17fd6d611bc4e30ghsax_refsource_MISCWEB
- github.com/haxtheweb/issues/security/advisories/GHSA-59g8-h59f-8hjpghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.