From aad5e46b312db086cef693ea1bae118e88e6ca41 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 03:29:23 +0000 Subject: [PATCH] fix(dashboard): whitelist exposed torch-config.json fields Restrict the /torch-config.json endpoint to only return fields required by the dashboard frontend (dashboard config excluding auth, and nostrLock namespace/relays). This prevents exposure of sensitive internal configuration such as rosters, scheduler settings, and full relay lists. - Modify src/dashboard.mjs to sanitize config using an allowlist approach. - Add test/dashboard-config-exposure.test.mjs to verify sensitive fields are stripped. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/dashboard.mjs | 20 +++- test/dashboard-config-exposure.test.mjs | 116 ++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 test/dashboard-config-exposure.test.mjs diff --git a/src/dashboard.mjs b/src/dashboard.mjs index 740357a..6c93ee2 100644 --- a/src/dashboard.mjs +++ b/src/dashboard.mjs @@ -105,14 +105,24 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0. const rawConfig = JSON.parse(configContent); const safeConfig = parseTorchConfig(rawConfig); - // Security: Sanitize sensitive fields - if (safeConfig.dashboard) { - delete safeConfig.dashboard.auth; + // Security: Whitelist only fields required by the dashboard frontend + const publicConfig = { + dashboard: safeConfig.dashboard ? { ...safeConfig.dashboard } : {}, + nostrLock: safeConfig.nostrLock + ? { + namespace: safeConfig.nostrLock.namespace, + relays: safeConfig.nostrLock.relays, + } + : {}, + }; + + // Remove sensitive dashboard auth + if (publicConfig.dashboard.auth) { + delete publicConfig.dashboard.auth; } - delete safeConfig.configPath; res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(safeConfig, null, 2)); + res.end(JSON.stringify(publicConfig, null, 2)); return; } catch (err) { console.error('Error parsing torch-config.json:', err); diff --git a/test/dashboard-config-exposure.test.mjs b/test/dashboard-config-exposure.test.mjs new file mode 100644 index 0000000..cc570a3 --- /dev/null +++ b/test/dashboard-config-exposure.test.mjs @@ -0,0 +1,116 @@ + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; +import { cmdDashboard } from '../src/dashboard.mjs'; + +const tempConfigPath = path.resolve('torch-config-exposure-test.json'); + +const SENSITIVE_CONFIG = { + nostrLock: { + namespace: 'test-namespace', + relays: ['wss://relay.example.com'], + dailyRoster: ['user1', 'user2'], // Sensitive roster + weeklyRoster: ['user3'], + ttlSeconds: 3600, + queryTimeoutMs: 5000, + publishTimeoutMs: 5000, + minSuccessfulRelayPublishes: 2, + minActiveRelayPool: 1, + }, + dashboard: { + auth: 'secret-auth-string', // stripped by auth check + relays: ['wss://dashboard.relay.example.com'], + }, + scheduler: { + paused: { + daily: ['user1'], + }, + firstPromptByCadence: { + daily: 'some prompt', + }, + }, +}; + +test('Dashboard Configuration Exposure Security', async (t) => { + // Setup temp config + fs.writeFileSync(tempConfigPath, JSON.stringify(SENSITIVE_CONFIG, null, 2)); + + // Override env to use this config + const originalEnvPath = process.env.TORCH_CONFIG_PATH; + const originalAuth = process.env.TORCH_DASHBOARD_AUTH; + + process.env.TORCH_CONFIG_PATH = tempConfigPath; + // Disable auth via env so we can access the endpoint without credentials + // The vulnerability is about exposure of *other* fields. + // We need to bypass the basic auth middleware to test the JSON response. + // However, cmdDashboard checks config.dashboard.auth too. + // If we want to test that auth is stripped from JSON, we should enable auth but provide credentials. + // OR we can just test the sanitization logic which runs regardless of how we got there? + // No, the endpoint logic runs after auth check. + + // To keep it simple: we want to verify that IF we can access the endpoint, the sensitive data is gone. + // We can simulate an authorized request or disable auth. + // If we disable auth in config, we can access it. + + // Let's modify the config for the test to NOT have auth, so we can access it freely. + // We want to verify that things like 'dailyRoster' are stripped. + const configForTest = { ...SENSITIVE_CONFIG }; + delete configForTest.dashboard.auth; + fs.writeFileSync(tempConfigPath, JSON.stringify(configForTest, null, 2)); + + let server; + let serverPort; + + // Cleanup function + t.after(() => { + if (server) server.close(); + if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath); + if (originalEnvPath) process.env.TORCH_CONFIG_PATH = originalEnvPath; + else delete process.env.TORCH_CONFIG_PATH; + }); + + // Start server + await new Promise((resolve) => { + cmdDashboard(0).then((srv) => { + server = srv; + serverPort = server.address().port; + resolve(); + }); + }); + + // Fetch config + const responseBody = await new Promise((resolve, reject) => { + http.get(`http://localhost:${serverPort}/torch-config.json`, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => resolve(data)); + }).on('error', reject); + }); + + let exposedConfig; + try { + exposedConfig = JSON.parse(responseBody); + } catch (e) { + assert.fail(`Failed to parse JSON response: ${responseBody}`); + } + + // 1. Assert sensitive fields are NOT present + assert.equal(exposedConfig.nostrLock?.dailyRoster, undefined, 'dailyRoster should NOT be exposed'); + assert.equal(exposedConfig.nostrLock?.weeklyRoster, undefined, 'weeklyRoster should NOT be exposed'); + assert.equal(exposedConfig.nostrLock?.relayFallbacks, undefined, 'relayFallbacks should NOT be exposed'); + assert.equal(exposedConfig.scheduler, undefined, 'scheduler config should NOT be exposed'); + assert.equal(exposedConfig.raw, undefined, 'raw config should NOT be exposed'); + + // 2. Assert required fields ARE present + assert.ok(exposedConfig.dashboard, 'dashboard config should be present'); + // Note: cmdDashboard loads from file, parseTorchConfig normalizes keys. + // check dashboard.relays + assert.deepEqual(exposedConfig.dashboard.relays, ['wss://dashboard.relay.example.com'], 'dashboard relays should be present'); + + assert.ok(exposedConfig.nostrLock, 'nostrLock config should be present'); + assert.equal(exposedConfig.nostrLock.namespace, 'test-namespace', 'nostrLock namespace should be present'); + assert.deepEqual(exposedConfig.nostrLock.relays, ['wss://relay.example.com'], 'nostrLock relays should be present'); +});