From 093ef3aa274d7538d88a6e6773fc29d96a86cf86 Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 16 Mar 2026 15:52:30 -0400 Subject: [PATCH 1/3] fixing gh code inspector issues --- server/middleware/index.js | 24 ++++++++++++++++++------ server/routes/dxcluster.js | 9 ++++++++- server/routes/dxpeditions.js | 9 ++++++--- server/routes/pskreporter.js | 10 +++++----- server/routes/wsjtx.js | 5 +++++ 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/server/middleware/index.js b/server/middleware/index.js index 06cb6cab..bf3e9f98 100644 --- a/server/middleware/index.js +++ b/server/middleware/index.js @@ -32,14 +32,26 @@ function applyMiddleware(app, ctx) { } // Security: Helmet - // CSP is intentionally disabled — the app loads scripts, styles, images, and data - // from dozens of external services (Leaflet CDN, Google Fonts, Open-Meteo, NOAA, - // NASA SDO/GIBS, PSKReporter, tile CDNs, etc.). A restrictive CSP breaks everything. - // All other Helmet protections (X-Content-Type-Options, HSTS, X-Frame-Options, etc.) - // remain active. + // CSP uses a permissive HTTPS-only policy — the app loads scripts, styles, images, + // and data from dozens of external services (Leaflet CDN, Google Fonts, Open-Meteo, + // NOAA, NASA SDO/GIBS, PSKReporter, tile CDNs, etc.). A restrictive CSP breaks + // everything. All other Helmet protections (X-Content-Type-Options, HSTS, + // X-Frame-Options, etc.) remain active. app.use( helmet({ - contentSecurityPolicy: false, // eslint-disable-line -- see comment above + contentSecurityPolicy: { + useDefaults: true, + directives: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'", 'https:', "'unsafe-inline'"], + styleSrc: ["'self'", 'https:', "'unsafe-inline'"], + imgSrc: ["'self'", 'data:', 'https:', 'blob:'], + connectSrc: ["'self'", 'https:', 'wss:'], + fontSrc: ["'self'", 'https:', 'data:'], + workerSrc: ["'self'", 'blob:'], + frameAncestors: ["'self'"], + }, + }, crossOriginEmbedderPolicy: false, // Required for cross-origin tile loading }), ); diff --git a/server/routes/dxcluster.js b/server/routes/dxcluster.js index 8c46fa2d..03600ce6 100644 --- a/server/routes/dxcluster.js +++ b/server/routes/dxcluster.js @@ -264,8 +264,15 @@ module.exports = function (app, ctx) { // SECURITY: session.node.host is always the resolved IP from validateCustomHost() // which rejects private/reserved addresses and prevents DNS rebinding (TOCTOU). // See the /api/dxcluster/paths handler where resolvedHost = hostCheck.resolvedIP. + // Additional safeguard: only allow literal IP addresses. + if (!session.node || typeof session.node.host !== 'string' || net.isIP(session.node.host) === 0) { + logWarn(`[DX Cluster] Refusing to connect to non-IP host: ${String(session.node?.host)}`); + session.connected = false; + session.connecting = false; + return; + } + client.connect(session.node.port, session.node.host, () => { - // CodeQL: validated above session.connected = true; session.connecting = false; session.lastConnectedAt = Date.now(); diff --git a/server/routes/dxpeditions.js b/server/routes/dxpeditions.js index d99beb57..e9df4430 100644 --- a/server/routes/dxpeditions.js +++ b/server/routes/dxpeditions.js @@ -36,14 +36,17 @@ module.exports = function (app, ctx) { let prev; do { prev = text; - text = text.replace(/]*>[\s\S]*?<\/script>/gi, ''); + text = text.replace(/]*>[\s\S]*?<\/script(?:\s[^>]*)?>/gi, ''); } while (text !== prev); do { prev = text; - text = text.replace(/]*>[\s\S]*?<\/style>/gi, ''); + text = text.replace(/]*>[\s\S]*?<\/style(?:\s[^>]*)?>/gi, ''); } while (text !== prev); // Strip any remaining opening script/style tags (malformed HTML) - text = text.replace(/]*>/gi, '').replace(/]*>/gi, ''); + do { + prev = text; + text = text.replace(/]*>/gi, '').replace(/]*>/gi, ''); + } while (text !== prev); text = text .replace(//gi, '\n') // Convert br to newlines .replace(/<[^>]+>/g, ' ') // Remove all HTML tags diff --git a/server/routes/pskreporter.js b/server/routes/pskreporter.js index d8c1ca29..b87043fd 100644 --- a/server/routes/pskreporter.js +++ b/server/routes/pskreporter.js @@ -492,7 +492,7 @@ module.exports = function (app, ctx) { // "Connection closed" errors are expected during reconnects — // the on('connect') handler will re-subscribe all active callsigns if (err.message && err.message.includes('onnection closed')) return; - console.error(`[PSK-MQTT] Subscribe error for ${call}:`, err.message); + console.error('[PSK-MQTT] Subscribe error for %s:', call, err.message); } }); } @@ -504,7 +504,7 @@ module.exports = function (app, ctx) { pskMqtt.client.unsubscribe([txTopic, rxTopic], (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error(`[PSK-MQTT] Unsubscribe error for ${call}:`, err.message); + console.error('[PSK-MQTT] Unsubscribe error for %s:', call, err.message); } }); } @@ -525,9 +525,9 @@ module.exports = function (app, ctx) { pskMqtt.client.subscribe([txTopic, rxTopic], { qos: 0 }, (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error(`[PSK-MQTT] Grid subscribe error for ${grid}:`, err.message); + console.error('[PSK-MQTT] Grid subscribe error for %s:', grid, err.message); } else { - console.log(`[PSK-MQTT] Subscribed grid ${grid}`); + console.log('[PSK-MQTT] Subscribed grid %s', grid); } }); } @@ -539,7 +539,7 @@ module.exports = function (app, ctx) { pskMqtt.client.unsubscribe([txTopic, rxTopic], (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error(`[PSK-MQTT] Grid unsubscribe error for ${grid}:`, err.message); + console.error('[PSK-MQTT] Grid unsubscribe error for %s:', grid, err.message); } }); } diff --git a/server/routes/wsjtx.js b/server/routes/wsjtx.js index 36f9d4a0..647edad6 100644 --- a/server/routes/wsjtx.js +++ b/server/routes/wsjtx.js @@ -481,6 +481,11 @@ module.exports = function (app, ctx) { // Reject dangerous msg.id values to prevent prototype pollution on state.clients if (msg.id && !isValidSessionId(msg.id)) return; + // Ensure clients is a prototype-less object to prevent prototype pollution + if (!state.clients || Object.getPrototypeOf(state.clients) !== null) { + state.clients = Object.assign(Object.create(null), state.clients || {}); + } + switch (msg.type) { case WSJTX_MSG.HEARTBEAT: { state.clients[msg.id] = { From 8a9e528e3c0335bc7f02aeceb24b2fe97a20dbe2 Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 16 Mar 2026 16:20:13 -0400 Subject: [PATCH 2/3] Revert "fixing gh code inspector issues" This reverts commit 093ef3aa274d7538d88a6e6773fc29d96a86cf86. --- server/middleware/index.js | 24 ++++++------------------ server/routes/dxcluster.js | 9 +-------- server/routes/dxpeditions.js | 9 +++------ server/routes/pskreporter.js | 10 +++++----- server/routes/wsjtx.js | 5 ----- 5 files changed, 15 insertions(+), 42 deletions(-) diff --git a/server/middleware/index.js b/server/middleware/index.js index bf3e9f98..06cb6cab 100644 --- a/server/middleware/index.js +++ b/server/middleware/index.js @@ -32,26 +32,14 @@ function applyMiddleware(app, ctx) { } // Security: Helmet - // CSP uses a permissive HTTPS-only policy — the app loads scripts, styles, images, - // and data from dozens of external services (Leaflet CDN, Google Fonts, Open-Meteo, - // NOAA, NASA SDO/GIBS, PSKReporter, tile CDNs, etc.). A restrictive CSP breaks - // everything. All other Helmet protections (X-Content-Type-Options, HSTS, - // X-Frame-Options, etc.) remain active. + // CSP is intentionally disabled — the app loads scripts, styles, images, and data + // from dozens of external services (Leaflet CDN, Google Fonts, Open-Meteo, NOAA, + // NASA SDO/GIBS, PSKReporter, tile CDNs, etc.). A restrictive CSP breaks everything. + // All other Helmet protections (X-Content-Type-Options, HSTS, X-Frame-Options, etc.) + // remain active. app.use( helmet({ - contentSecurityPolicy: { - useDefaults: true, - directives: { - defaultSrc: ["'self'"], - scriptSrc: ["'self'", 'https:', "'unsafe-inline'"], - styleSrc: ["'self'", 'https:', "'unsafe-inline'"], - imgSrc: ["'self'", 'data:', 'https:', 'blob:'], - connectSrc: ["'self'", 'https:', 'wss:'], - fontSrc: ["'self'", 'https:', 'data:'], - workerSrc: ["'self'", 'blob:'], - frameAncestors: ["'self'"], - }, - }, + contentSecurityPolicy: false, // eslint-disable-line -- see comment above crossOriginEmbedderPolicy: false, // Required for cross-origin tile loading }), ); diff --git a/server/routes/dxcluster.js b/server/routes/dxcluster.js index 03600ce6..8c46fa2d 100644 --- a/server/routes/dxcluster.js +++ b/server/routes/dxcluster.js @@ -264,15 +264,8 @@ module.exports = function (app, ctx) { // SECURITY: session.node.host is always the resolved IP from validateCustomHost() // which rejects private/reserved addresses and prevents DNS rebinding (TOCTOU). // See the /api/dxcluster/paths handler where resolvedHost = hostCheck.resolvedIP. - // Additional safeguard: only allow literal IP addresses. - if (!session.node || typeof session.node.host !== 'string' || net.isIP(session.node.host) === 0) { - logWarn(`[DX Cluster] Refusing to connect to non-IP host: ${String(session.node?.host)}`); - session.connected = false; - session.connecting = false; - return; - } - client.connect(session.node.port, session.node.host, () => { + // CodeQL: validated above session.connected = true; session.connecting = false; session.lastConnectedAt = Date.now(); diff --git a/server/routes/dxpeditions.js b/server/routes/dxpeditions.js index e9df4430..d99beb57 100644 --- a/server/routes/dxpeditions.js +++ b/server/routes/dxpeditions.js @@ -36,17 +36,14 @@ module.exports = function (app, ctx) { let prev; do { prev = text; - text = text.replace(/]*>[\s\S]*?<\/script(?:\s[^>]*)?>/gi, ''); + text = text.replace(/]*>[\s\S]*?<\/script>/gi, ''); } while (text !== prev); do { prev = text; - text = text.replace(/]*>[\s\S]*?<\/style(?:\s[^>]*)?>/gi, ''); + text = text.replace(/]*>[\s\S]*?<\/style>/gi, ''); } while (text !== prev); // Strip any remaining opening script/style tags (malformed HTML) - do { - prev = text; - text = text.replace(/]*>/gi, '').replace(/]*>/gi, ''); - } while (text !== prev); + text = text.replace(/]*>/gi, '').replace(/]*>/gi, ''); text = text .replace(//gi, '\n') // Convert br to newlines .replace(/<[^>]+>/g, ' ') // Remove all HTML tags diff --git a/server/routes/pskreporter.js b/server/routes/pskreporter.js index b87043fd..d8c1ca29 100644 --- a/server/routes/pskreporter.js +++ b/server/routes/pskreporter.js @@ -492,7 +492,7 @@ module.exports = function (app, ctx) { // "Connection closed" errors are expected during reconnects — // the on('connect') handler will re-subscribe all active callsigns if (err.message && err.message.includes('onnection closed')) return; - console.error('[PSK-MQTT] Subscribe error for %s:', call, err.message); + console.error(`[PSK-MQTT] Subscribe error for ${call}:`, err.message); } }); } @@ -504,7 +504,7 @@ module.exports = function (app, ctx) { pskMqtt.client.unsubscribe([txTopic, rxTopic], (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error('[PSK-MQTT] Unsubscribe error for %s:', call, err.message); + console.error(`[PSK-MQTT] Unsubscribe error for ${call}:`, err.message); } }); } @@ -525,9 +525,9 @@ module.exports = function (app, ctx) { pskMqtt.client.subscribe([txTopic, rxTopic], { qos: 0 }, (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error('[PSK-MQTT] Grid subscribe error for %s:', grid, err.message); + console.error(`[PSK-MQTT] Grid subscribe error for ${grid}:`, err.message); } else { - console.log('[PSK-MQTT] Subscribed grid %s', grid); + console.log(`[PSK-MQTT] Subscribed grid ${grid}`); } }); } @@ -539,7 +539,7 @@ module.exports = function (app, ctx) { pskMqtt.client.unsubscribe([txTopic, rxTopic], (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error('[PSK-MQTT] Grid unsubscribe error for %s:', grid, err.message); + console.error(`[PSK-MQTT] Grid unsubscribe error for ${grid}:`, err.message); } }); } diff --git a/server/routes/wsjtx.js b/server/routes/wsjtx.js index 647edad6..36f9d4a0 100644 --- a/server/routes/wsjtx.js +++ b/server/routes/wsjtx.js @@ -481,11 +481,6 @@ module.exports = function (app, ctx) { // Reject dangerous msg.id values to prevent prototype pollution on state.clients if (msg.id && !isValidSessionId(msg.id)) return; - // Ensure clients is a prototype-less object to prevent prototype pollution - if (!state.clients || Object.getPrototypeOf(state.clients) !== null) { - state.clients = Object.assign(Object.create(null), state.clients || {}); - } - switch (msg.type) { case WSJTX_MSG.HEARTBEAT: { state.clients[msg.id] = { From 9c24fd74cb4afafafd1808aa810299d2c7883b12 Mon Sep 17 00:00:00 2001 From: accius Date: Mon, 16 Mar 2026 16:24:34 -0400 Subject: [PATCH 3/3] gh fix --- server/routes/dxpeditions.js | 9 ++++++--- server/routes/pskreporter.js | 10 +++++----- server/routes/wsjtx.js | 5 +++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/server/routes/dxpeditions.js b/server/routes/dxpeditions.js index d99beb57..e9df4430 100644 --- a/server/routes/dxpeditions.js +++ b/server/routes/dxpeditions.js @@ -36,14 +36,17 @@ module.exports = function (app, ctx) { let prev; do { prev = text; - text = text.replace(/]*>[\s\S]*?<\/script>/gi, ''); + text = text.replace(/]*>[\s\S]*?<\/script(?:\s[^>]*)?>/gi, ''); } while (text !== prev); do { prev = text; - text = text.replace(/]*>[\s\S]*?<\/style>/gi, ''); + text = text.replace(/]*>[\s\S]*?<\/style(?:\s[^>]*)?>/gi, ''); } while (text !== prev); // Strip any remaining opening script/style tags (malformed HTML) - text = text.replace(/]*>/gi, '').replace(/]*>/gi, ''); + do { + prev = text; + text = text.replace(/]*>/gi, '').replace(/]*>/gi, ''); + } while (text !== prev); text = text .replace(//gi, '\n') // Convert br to newlines .replace(/<[^>]+>/g, ' ') // Remove all HTML tags diff --git a/server/routes/pskreporter.js b/server/routes/pskreporter.js index d8c1ca29..b87043fd 100644 --- a/server/routes/pskreporter.js +++ b/server/routes/pskreporter.js @@ -492,7 +492,7 @@ module.exports = function (app, ctx) { // "Connection closed" errors are expected during reconnects — // the on('connect') handler will re-subscribe all active callsigns if (err.message && err.message.includes('onnection closed')) return; - console.error(`[PSK-MQTT] Subscribe error for ${call}:`, err.message); + console.error('[PSK-MQTT] Subscribe error for %s:', call, err.message); } }); } @@ -504,7 +504,7 @@ module.exports = function (app, ctx) { pskMqtt.client.unsubscribe([txTopic, rxTopic], (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error(`[PSK-MQTT] Unsubscribe error for ${call}:`, err.message); + console.error('[PSK-MQTT] Unsubscribe error for %s:', call, err.message); } }); } @@ -525,9 +525,9 @@ module.exports = function (app, ctx) { pskMqtt.client.subscribe([txTopic, rxTopic], { qos: 0 }, (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error(`[PSK-MQTT] Grid subscribe error for ${grid}:`, err.message); + console.error('[PSK-MQTT] Grid subscribe error for %s:', grid, err.message); } else { - console.log(`[PSK-MQTT] Subscribed grid ${grid}`); + console.log('[PSK-MQTT] Subscribed grid %s', grid); } }); } @@ -539,7 +539,7 @@ module.exports = function (app, ctx) { pskMqtt.client.unsubscribe([txTopic, rxTopic], (err) => { if (err) { if (err.message && err.message.includes('onnection closed')) return; - console.error(`[PSK-MQTT] Grid unsubscribe error for ${grid}:`, err.message); + console.error('[PSK-MQTT] Grid unsubscribe error for %s:', grid, err.message); } }); } diff --git a/server/routes/wsjtx.js b/server/routes/wsjtx.js index 36f9d4a0..647edad6 100644 --- a/server/routes/wsjtx.js +++ b/server/routes/wsjtx.js @@ -481,6 +481,11 @@ module.exports = function (app, ctx) { // Reject dangerous msg.id values to prevent prototype pollution on state.clients if (msg.id && !isValidSessionId(msg.id)) return; + // Ensure clients is a prototype-less object to prevent prototype pollution + if (!state.clients || Object.getPrototypeOf(state.clients) !== null) { + state.clients = Object.assign(Object.create(null), state.clients || {}); + } + switch (msg.type) { case WSJTX_MSG.HEARTBEAT: { state.clients[msg.id] = {