Sanitize strict diagnostics storage failures

The strict ConfigStore readers previously propagated native filesystem and JSON parser errors. On Node 24, malformed JSON can be quoted in the parser message, so an opaque value from a corrupt history file could cross the collector and diagnostics-agent boundary unchanged when it was not one of the configured secrets.

Map strict config and history read, parse, and shape failures to a closed set of constant content-free errors. Keep separate invalid, read-failed, and history-not-found codes so the pre-migration history fallback remains limited to a genuinely absent dedicated file. Do not retain native errors or raw input as causes, while leaving credential decryption failures and the recovery-oriented UI readers unchanged.

Add boundary coverage for corrupt config JSON, invalid shapes, and read failures, plus an end-to-end ConfigStore-to-collectors-to-agent regression proving get_history, list_errors, and server_health never return a planted opaque history value.
This commit is contained in:
Sucukdeluxe
2026-08-13 23:21:08 +02:00
parent a8065a2313
commit e124ab2923
2 changed files with 131 additions and 15 deletions
+63 -13
View File
@@ -132,6 +132,20 @@ const HISTORY_RETENTION_OPTIONS = [
{ value: '100', label: 'Letzte 100 Uploads' }
];
const DIAGNOSTIC_ERROR_MESSAGES = Object.freeze({
DIAGNOSTIC_CONFIG_READ_FAILED: 'Die Diagnosekonfiguration konnte nicht gelesen werden',
DIAGNOSTIC_CONFIG_INVALID: 'Die Diagnosekonfiguration ist ungültig',
DIAGNOSTIC_HISTORY_NOT_FOUND: 'Die Diagnoseverlaufsdatei wurde nicht gefunden',
DIAGNOSTIC_HISTORY_READ_FAILED: 'Die Diagnoseverlaufsdatei konnte nicht gelesen werden',
DIAGNOSTIC_HISTORY_INVALID: 'Die Diagnoseverlaufsdatei ist ungültig'
});
function diagnosticStoreError(code) {
const error = new Error(DIAGNOSTIC_ERROR_MESSAGES[code]);
error.code = code;
return error;
}
function batchTimestampMs(batch) {
const raw = batch && batch.timestamp;
if (raw === null || raw === undefined || raw === '') return null;
@@ -225,12 +239,27 @@ class ConfigStore {
}
_readHistoryFileStrict() {
const raw = fs.readFileSync(this.historyPath, 'utf-8');
if (!raw || raw.trim().length < 2) throw new Error('Die Diagnoseverlaufsdatei ist ungültig');
const parsed = JSON.parse(raw);
let raw;
try {
raw = fs.readFileSync(this.historyPath, 'utf-8');
} catch (error) {
if (error && error.code === 'ENOENT') {
throw diagnosticStoreError('DIAGNOSTIC_HISTORY_NOT_FOUND');
}
throw diagnosticStoreError('DIAGNOSTIC_HISTORY_READ_FAILED');
}
if (!raw || raw.trim().length < 2) {
throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID');
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID');
}
if (Array.isArray(parsed)) return parsed;
if (parsed && Array.isArray(parsed.history)) return parsed.history;
throw new Error('Die Diagnoseverlaufsdatei ist ungültig');
throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID');
}
_writeHistoryFileDurable(arr) {
@@ -351,6 +380,31 @@ class ConfigStore {
return JSON.parse(raw);
}
_readConfigFileStrict() {
let raw;
try {
raw = fs.readFileSync(this.filePath, 'utf-8');
} catch {
throw diagnosticStoreError('DIAGNOSTIC_CONFIG_READ_FAILED');
}
if (!raw || raw.trim().length < 2) {
throw diagnosticStoreError('DIAGNOSTIC_CONFIG_INVALID');
}
let data;
try {
data = JSON.parse(raw);
} catch {
throw diagnosticStoreError('DIAGNOSTIC_CONFIG_INVALID');
}
if (!data || typeof data !== 'object' || Array.isArray(data) ||
!data.hosters || typeof data.hosters !== 'object' || Array.isArray(data.hosters) ||
!data.globalSettings || typeof data.globalSettings !== 'object' || Array.isArray(data.globalSettings) ||
(data.history !== undefined && !Array.isArray(data.history))) {
throw diagnosticStoreError('DIAGNOSTIC_CONFIG_INVALID');
}
return data;
}
_clone(obj) {
try { return structuredClone(obj); }
catch { return JSON.parse(JSON.stringify(obj)); }
@@ -415,13 +469,7 @@ class ConfigStore {
let data = null;
if (strict) {
data = this._readAndParse(this.filePath);
if (!data || typeof data !== 'object' || Array.isArray(data) ||
!data.hosters || typeof data.hosters !== 'object' || Array.isArray(data.hosters) ||
!data.globalSettings || typeof data.globalSettings !== 'object' || Array.isArray(data.globalSettings) ||
(data.history !== undefined && !Array.isArray(data.history))) {
throw new Error('Die Diagnosekonfiguration ist ungültig');
}
data = this._readConfigFileStrict();
} else {
// Try main config
try { data = this._readAndParse(this.filePath); } catch {}
@@ -737,10 +785,12 @@ class ConfigStore {
try {
return this._readHistoryFileStrict();
} catch (error) {
if (!error || error.code !== 'ENOENT') throw error;
if (!error || error.code !== 'DIAGNOSTIC_HISTORY_NOT_FOUND') throw error;
}
const config = this.loadDiagnosticsConfig();
if (!Array.isArray(config.history)) throw new Error('Der Diagnoseverlauf ist ungültig');
if (!Array.isArray(config.history)) {
throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID');
}
return config.history;
}
+68 -2
View File
@@ -17,10 +17,36 @@ Module._load = function load(request, parent, isMain) {
const ConfigStore = require('../lib/config-store');
require('../lib/secret-store').encryptField('test-initialization');
Module._load = originalLoad;
const { createCollectors } = require('../lib/diagnostics-collectors');
const { createAgent } = require('../lib/diagnostics-agent');
const support = require('../lib/support-bundle');
const stats = require('../lib/stats');
let tmpDir;
let store;
function thrownBy(fn) {
try {
fn();
} catch (error) {
return error;
}
assert.fail('Expected function to throw');
}
function createDiagnosticsAgent(configStore, logDir) {
return createAgent(createCollectors({
loadConfig: () => configStore.loadDiagnosticsConfig(),
loadHistory: () => configStore.loadDiagnosticsHistory(),
getAllLogPaths: () => ({ logDir }),
support,
stats,
appInfo: () => ({}),
systemInfo: () => ({}),
agentInfo: () => ({})
}));
}
function createStore() {
const fakeApp = {
isPackaged: false,
@@ -685,6 +711,27 @@ describe('ConfigStore', () => {
assert.throws(() => store.loadDiagnosticsConfig());
});
it('strict diagnostics config errors never expose malformed JSON or file paths', () => {
const opaque = 'opaque-config-42';
fs.writeFileSync(store.filePath, opaque, 'utf-8');
let error = thrownBy(() => store.loadDiagnosticsConfig());
assert.equal(error.code, 'DIAGNOSTIC_CONFIG_INVALID');
assert.equal(error.message, 'Die Diagnosekonfiguration ist ungültig');
assert.ok(!error.message.includes(opaque));
fs.writeFileSync(store.filePath, '{}', 'utf-8');
error = thrownBy(() => store.loadDiagnosticsConfig());
assert.equal(error.code, 'DIAGNOSTIC_CONFIG_INVALID');
assert.equal(error.message, 'Die Diagnosekonfiguration ist ungültig');
fs.rmSync(store.filePath);
error = thrownBy(() => store.loadDiagnosticsConfig());
assert.equal(error.code, 'DIAGNOSTIC_CONFIG_READ_FAILED');
assert.equal(error.message, 'Die Diagnosekonfiguration konnte nicht gelesen werden');
assert.ok(!error.message.includes(store.filePath));
});
it('strict diagnostics config loading decrypts current credentials and propagates decryption failures', () => {
assert.equal(typeof store.loadDiagnosticsConfig, 'function');
@@ -781,12 +828,31 @@ describe('ConfigStore history split (electron-history.json)', () => {
for (const invalidHistory of ['null', '{}', '{"history":null}', '{broken-history']) {
fs.writeFileSync(s.historyPath, invalidHistory, 'utf-8');
assert.throws(() => s.loadDiagnosticsHistory());
const error = thrownBy(() => s.loadDiagnosticsHistory());
assert.equal(error.code, 'DIAGNOSTIC_HISTORY_INVALID');
assert.equal(error.message, 'Die Diagnoseverlaufsdatei ist ungültig');
assert.ok(!error.message.includes(invalidHistory));
}
fs.rmSync(s.historyPath);
fs.mkdirSync(s.historyPath);
assert.throws(() => s.loadDiagnosticsHistory());
const error = thrownBy(() => s.loadDiagnosticsHistory());
assert.equal(error.code, 'DIAGNOSTIC_HISTORY_READ_FAILED');
assert.equal(error.message, 'Die Diagnoseverlaufsdatei konnte nicht gelesen werden');
assert.ok(!error.message.includes(s.historyPath));
});
it('real diagnostics agent never returns opaque corrupt dedicated history content', () => {
const opaque = 'opaque42';
writeConfigWithHistory(0);
fs.writeFileSync(s.historyPath, opaque, 'utf-8');
const agent = createDiagnosticsAgent(s, dir);
for (const operation of ['get_history', 'list_errors', 'server_health']) {
const response = agent.handle(operation, {});
assert.deepEqual(response, { ok: false, error: 'Die Diagnoseverlaufsdatei ist ungültig' });
assert.ok(!JSON.stringify(response).includes(opaque));
}
});
it('strict diagnostics history never falls back to stale config history when a dedicated file exists', () => {