mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
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:
1 parent
cea50da512
commit
aad5e46b31
2 files changed
+131
-5
No files matched your search
+15
-5
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user