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:
+63
-13
@@ -132,6 +132,20 @@ const HISTORY_RETENTION_OPTIONS = [
|
|||||||
{ value: '100', label: 'Letzte 100 Uploads' }
|
{ 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) {
|
function batchTimestampMs(batch) {
|
||||||
const raw = batch && batch.timestamp;
|
const raw = batch && batch.timestamp;
|
||||||
if (raw === null || raw === undefined || raw === '') return null;
|
if (raw === null || raw === undefined || raw === '') return null;
|
||||||
@@ -225,12 +239,27 @@ class ConfigStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_readHistoryFileStrict() {
|
_readHistoryFileStrict() {
|
||||||
const raw = fs.readFileSync(this.historyPath, 'utf-8');
|
let raw;
|
||||||
if (!raw || raw.trim().length < 2) throw new Error('Die Diagnoseverlaufsdatei ist ungültig');
|
try {
|
||||||
const parsed = JSON.parse(raw);
|
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 (Array.isArray(parsed)) return parsed;
|
||||||
if (parsed && Array.isArray(parsed.history)) return parsed.history;
|
if (parsed && Array.isArray(parsed.history)) return parsed.history;
|
||||||
throw new Error('Die Diagnoseverlaufsdatei ist ungültig');
|
throw diagnosticStoreError('DIAGNOSTIC_HISTORY_INVALID');
|
||||||
}
|
}
|
||||||
|
|
||||||
_writeHistoryFileDurable(arr) {
|
_writeHistoryFileDurable(arr) {
|
||||||
@@ -351,6 +380,31 @@ class ConfigStore {
|
|||||||
return JSON.parse(raw);
|
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) {
|
_clone(obj) {
|
||||||
try { return structuredClone(obj); }
|
try { return structuredClone(obj); }
|
||||||
catch { return JSON.parse(JSON.stringify(obj)); }
|
catch { return JSON.parse(JSON.stringify(obj)); }
|
||||||
@@ -415,13 +469,7 @@ class ConfigStore {
|
|||||||
|
|
||||||
let data = null;
|
let data = null;
|
||||||
if (strict) {
|
if (strict) {
|
||||||
data = this._readAndParse(this.filePath);
|
data = this._readConfigFileStrict();
|
||||||
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');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Try main config
|
// Try main config
|
||||||
try { data = this._readAndParse(this.filePath); } catch {}
|
try { data = this._readAndParse(this.filePath); } catch {}
|
||||||
@@ -737,10 +785,12 @@ class ConfigStore {
|
|||||||
try {
|
try {
|
||||||
return this._readHistoryFileStrict();
|
return this._readHistoryFileStrict();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!error || error.code !== 'ENOENT') throw error;
|
if (!error || error.code !== 'DIAGNOSTIC_HISTORY_NOT_FOUND') throw error;
|
||||||
}
|
}
|
||||||
const config = this.loadDiagnosticsConfig();
|
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;
|
return config.history;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,36 @@ Module._load = function load(request, parent, isMain) {
|
|||||||
const ConfigStore = require('../lib/config-store');
|
const ConfigStore = require('../lib/config-store');
|
||||||
require('../lib/secret-store').encryptField('test-initialization');
|
require('../lib/secret-store').encryptField('test-initialization');
|
||||||
Module._load = originalLoad;
|
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 tmpDir;
|
||||||
let store;
|
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() {
|
function createStore() {
|
||||||
const fakeApp = {
|
const fakeApp = {
|
||||||
isPackaged: false,
|
isPackaged: false,
|
||||||
@@ -685,6 +711,27 @@ describe('ConfigStore', () => {
|
|||||||
assert.throws(() => store.loadDiagnosticsConfig());
|
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', () => {
|
it('strict diagnostics config loading decrypts current credentials and propagates decryption failures', () => {
|
||||||
assert.equal(typeof store.loadDiagnosticsConfig, 'function');
|
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']) {
|
for (const invalidHistory of ['null', '{}', '{"history":null}', '{broken-history']) {
|
||||||
fs.writeFileSync(s.historyPath, invalidHistory, 'utf-8');
|
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.rmSync(s.historyPath);
|
||||||
fs.mkdirSync(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', () => {
|
it('strict diagnostics history never falls back to stale config history when a dedicated file exists', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user