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 <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-18 03:29:23 +00:00
1 parent cea50da512
commit aad5e46b31
2 files changed
+131 -5

No files matched your search

+15 -5
View File
@@ -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);
+116
View File
@@ -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');
});