Merge pull request #229 from PR0M3TH3AN/fix/dashboard-config-security-7655777063695938949

🔒 Fix security vulnerability: Sanitize torch-config.json exposure
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-17 13:09:02 -05:00
commit 8f51d74955
2 files changed
+170 -9

No files matched your search

+49 -9
View File
@@ -5,7 +5,7 @@ import http from 'node:http';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { DEFAULT_DASHBOARD_PORT } from './constants.mjs';
import { getDashboardAuth } from './torch-config.mjs';
import { getDashboardAuth, parseTorchConfig } from './torch-config.mjs';
function timingSafeCompare(a, b) {
const bufA = Buffer.from(a);
@@ -73,17 +73,57 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
}
// Special case: /torch-config.json
// Priority: User's CWD > Package default
// Priority: Env Var > User's CWD > Package default
if (pathname === '/torch-config.json') {
const userConfigPath = path.resolve(process.cwd(), 'torch-config.json');
if (await statSafe(userConfigPath)) {
res.writeHead(200, { 'Content-Type': 'application/json' });
fs.createReadStream(userConfigPath).pipe(res);
let configContent = null;
if (process.env.TORCH_CONFIG_PATH) {
const envPath = path.resolve(process.cwd(), process.env.TORCH_CONFIG_PATH);
try {
configContent = await fsp.readFile(envPath, 'utf8');
} catch {
// Explicitly provided config path not found, do not fall back
}
} else {
const userConfigPath = path.resolve(process.cwd(), 'torch-config.json');
try {
configContent = await fsp.readFile(userConfigPath, 'utf8');
} catch {
// Try package default
const packageConfigPath = path.join(packageRoot, 'torch-config.json');
try {
configContent = await fsp.readFile(packageConfigPath, 'utf8');
} catch {
// Not found in either location
}
}
}
if (configContent) {
try {
const rawConfig = JSON.parse(configContent);
const safeConfig = parseTorchConfig(rawConfig);
// Security: Sanitize sensitive fields
if (safeConfig.dashboard) {
delete safeConfig.dashboard.auth;
}
delete safeConfig.configPath;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(safeConfig, null, 2));
return;
} catch (err) {
console.error('Error parsing torch-config.json:', err);
res.writeHead(500);
res.end('Internal Server Error');
return;
}
} else {
res.writeHead(404);
res.end('Not Found');
return;
}
// If not found in CWD, fall through to serve from packageRoot (if it exists there)
// or return empty object if missing?
// Falling through means it looks for packageRoot/torch-config.json
}
// Security check: prevent directory traversal
+121
View File
@@ -0,0 +1,121 @@
import test from 'node:test';
import assert from 'node:assert';
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { cmdDashboard } from '../src/dashboard.mjs';
import { _resetTorchConfigCache } from '../src/torch-config.mjs';
const CONFIG_FILE = 'torch-config-test.json';
const TEST_PORT = 4176;
// Helper to fetch JSON
function fetchJson(url, auth) {
return new Promise((resolve, reject) => {
const options = {
headers: {}
};
if (auth) {
options.headers['Authorization'] = `Basic ${Buffer.from(auth).toString('base64')}`;
}
http.get(url, options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
}).on('error', reject);
});
}
test('Dashboard Configuration Security', async (t) => {
process.env.TORCH_CONFIG_PATH = CONFIG_FILE;
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
// Ensure clean state
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
_resetTorchConfigCache();
t.after(() => {
delete process.env.TORCH_CONFIG_PATH;
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
});
// Scenario 1: Unauthenticated access (no auth configured), sensitive fields in config
await t.test('sanitizes config when auth is disabled', async () => {
const sensitiveConfig = {
dashboard: {
relays: ["wss://public.relay.com"]
},
super_secret_key: "DO_NOT_LEAK",
nostrLock: {
relays: ["wss://lock.relay.com"]
}
};
fs.writeFileSync(configPath, JSON.stringify(sensitiveConfig, null, 2));
_resetTorchConfigCache();
const server = await cmdDashboard(TEST_PORT);
try {
const { statusCode, body } = await fetchJson(`http://127.0.0.1:${TEST_PORT}/torch-config.json`);
assert.strictEqual(statusCode, 200);
const json = JSON.parse(body);
// Check for leaks
// BEFORE FIX: This will likely FAIL because super_secret_key is present
if (json.super_secret_key) {
console.log('WARNING: super_secret_key leaked (Expected failure before fix)');
}
assert.strictEqual(json.super_secret_key, undefined, 'Unknown fields should be stripped');
// Check for valid fields
assert.deepStrictEqual(json.dashboard.relays, sensitiveConfig.dashboard.relays);
assert.deepStrictEqual(json.nostrLock.relays, sensitiveConfig.nostrLock.relays);
// Check that configPath is not leaked
assert.strictEqual(json.configPath, undefined, 'configPath should not be exposed');
} finally {
server.close();
// Wait for server close? cmdDashboard returns server.
}
});
// Scenario 2: Authenticated access, auth credentials in config
await t.test('sanitizes auth credentials even when authenticated', async () => {
const authConfig = {
dashboard: {
auth: "admin:password123",
relays: ["wss://secure.relay.com"]
}
};
fs.writeFileSync(configPath, JSON.stringify(authConfig, null, 2));
_resetTorchConfigCache();
const TEST_PORT_AUTH = TEST_PORT + 1;
const server = await cmdDashboard(TEST_PORT_AUTH);
try {
// 1. Verify 401 without auth
const res401 = await fetchJson(`http://127.0.0.1:${TEST_PORT_AUTH}/torch-config.json`);
assert.strictEqual(res401.statusCode, 401);
// 2. Verify 200 with auth
const { statusCode, body } = await fetchJson(`http://127.0.0.1:${TEST_PORT_AUTH}/torch-config.json`, "admin:password123");
assert.strictEqual(statusCode, 200);
const json = JSON.parse(body);
// Check for leaks
assert.strictEqual(json.dashboard.auth, undefined, 'Auth credentials should be stripped');
// Check for valid fields
assert.deepStrictEqual(json.dashboard.relays, authConfig.dashboard.relays);
} finally {
server.close();
}
});
});