mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
d7a2fe6562
Protect backup restores and session-authenticated notification writes from unsafe state changes, while clearing CodeQL false positives without weakening intentional TLS pinning. Refs CodeQL alerts #297-310 Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow)
164 lines
7.1 KiB
JavaScript
164 lines
7.1 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const config = require('../config/config');
|
|
const { createConsoleDeployGraph } = require('../lib/consoleDeployGraph');
|
|
const {
|
|
GITHUB_COMPARE_FILE_LIMIT,
|
|
isCompareLikelyTruncated,
|
|
isRetryableDownloadStatus,
|
|
getDownloadRetryDelayMs,
|
|
ensureGoServerSignalRelayPorts,
|
|
restoreServerBinaryBackup,
|
|
restoreFromBackup,
|
|
} = require('../services/updateService');
|
|
|
|
describe('updateService console sync helpers', () => {
|
|
test('detects truncated GitHub compare responses', () => {
|
|
expect(GITHUB_COMPARE_FILE_LIMIT).toBe(300);
|
|
expect(isCompareLikelyTruncated(299)).toBe(false);
|
|
expect(isCompareLikelyTruncated(300)).toBe(true);
|
|
expect(isCompareLikelyTruncated(450)).toBe(true);
|
|
});
|
|
|
|
test('resolves relative console require paths', () => {
|
|
const graph = createConsoleDeployGraph(require('path').join(__dirname, '..'));
|
|
expect(graph.resolveConsoleRequire('routes/auth.routes.js', '../services/serverAttestation'))
|
|
.toBe('services/serverAttestation.js');
|
|
expect(graph.resolveConsoleRequire('routes/index.js', './devices.routes'))
|
|
.toBe('routes/devices.routes.js');
|
|
expect(graph.resolveConsoleRequire('server.js', './routes'))
|
|
.toBe('routes/index.js');
|
|
expect(graph.resolveConsoleRequire('server.js', 'express')).toBeNull();
|
|
});
|
|
|
|
test('skips phantom routes.js when routes/index.js exists during repair scan', () => {
|
|
const graph = createConsoleDeployGraph(require('path').join(__dirname, '..'));
|
|
expect(graph.isResolvedByIndexModule('routes.js')).toBe(true);
|
|
expect(graph.isResolvedByIndexModule('routes/auth.routes.js')).toBe(false);
|
|
});
|
|
|
|
test('ignores require examples inside comments when scanning dependencies', () => {
|
|
const graph = createConsoleDeployGraph(require('path').join(__dirname, '..'));
|
|
const required = graph.collectConsoleRequiredFiles([
|
|
{ localPath: 'scripts/linux-ensure-console-user.js' },
|
|
]);
|
|
expect(required.has('scripts/linux-ensure-console-user.js')).toBe(true);
|
|
expect(required.has('scripts/scripts/linux-ensure-console-user.js')).toBe(false);
|
|
expect(required.has('routes.js')).toBe(false);
|
|
expect(required.has('routes/index.js')).toBe(true);
|
|
});
|
|
|
|
test('collects serverAttestation from auth.routes integrity seeds', () => {
|
|
const graph = createConsoleDeployGraph(require('path').join(__dirname, '..'));
|
|
const required = graph.collectConsoleRequiredFiles([
|
|
{ localPath: 'routes/auth.routes.js' },
|
|
]);
|
|
expect(required.has('routes/auth.routes.js')).toBe(true);
|
|
expect(required.has('services/serverAttestation.js')).toBe(true);
|
|
});
|
|
|
|
test('retries GitHub raw downloads on rate limit status codes', () => {
|
|
expect(isRetryableDownloadStatus(429)).toBe(true);
|
|
expect(isRetryableDownloadStatus(503)).toBe(true);
|
|
expect(isRetryableDownloadStatus(404)).toBe(false);
|
|
expect(getDownloadRetryDelayMs(1)).toBe(1000);
|
|
expect(getDownloadRetryDelayMs(2)).toBe(2000);
|
|
expect(getDownloadRetryDelayMs(6)).toBe(15000);
|
|
});
|
|
|
|
test('ensureGoServerSignalRelayPorts adds SIGNAL_PORT, RELAY_PORT, and GO_API_PORT (#219)', () => {
|
|
const unit = [
|
|
'[Service]',
|
|
'User=root',
|
|
'Environment=AUTH_DB_PATH=/opt/console/data/auth.db',
|
|
'ExecStart=/opt/betterdesk/betterdesk-server -mode all',
|
|
].join('\n');
|
|
const patched = ensureGoServerSignalRelayPorts(unit);
|
|
expect(patched.changed).toBe(true);
|
|
expect(patched.text).toMatch(/^Environment=SIGNAL_PORT=21116$/m);
|
|
expect(patched.text).toMatch(/^Environment=RELAY_PORT=21117$/m);
|
|
expect(patched.text).toMatch(/^Environment=GO_API_PORT=21114$/m);
|
|
|
|
const again = ensureGoServerSignalRelayPorts(patched.text);
|
|
expect(again.changed).toBe(false);
|
|
});
|
|
|
|
test('restores a validated server binary backup atomically', () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bd-binary-rollback-'));
|
|
const target = path.join(root, 'betterdesk-server.exe');
|
|
const backup = `${target}.bak.test`;
|
|
try {
|
|
fs.writeFileSync(target, 'new');
|
|
fs.writeFileSync(backup, 'old');
|
|
expect(restoreServerBinaryBackup(backup, target)).toMatchObject({ restored: true });
|
|
expect(fs.readFileSync(target, 'utf8')).toBe('old');
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('rejects a binary backup outside the target directory', () => {
|
|
expect(restoreServerBinaryBackup(
|
|
path.join(os.tmpdir(), 'betterdesk-server.exe.bak.test'),
|
|
path.join(os.tmpdir(), 'other', 'betterdesk-server.exe')
|
|
)).toMatchObject({
|
|
restored: false,
|
|
error: 'Server binary backup path failed validation',
|
|
});
|
|
});
|
|
|
|
test('rejects traversal paths from a backup manifest', () => {
|
|
const backupRoot = path.join(config.dataDir, 'backups');
|
|
const backupName = `pre-update-${Date.now()}-${process.pid}`;
|
|
const backupPath = path.join(backupRoot, backupName);
|
|
fs.mkdirSync(backupPath, { recursive: true });
|
|
fs.writeFileSync(path.join(backupPath, 'manifest.json'), JSON.stringify({
|
|
files: ['../outside.txt'],
|
|
}));
|
|
|
|
try {
|
|
expect(() => restoreFromBackup(backupName))
|
|
.toThrow('Invalid path in backup manifest');
|
|
} finally {
|
|
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('refuses to restore protected runtime files from a backup manifest', () => {
|
|
const backupRoot = path.join(config.dataDir, 'backups');
|
|
const backupName = `pre-update-${Date.now()}-${process.pid}`;
|
|
const backupPath = path.join(backupRoot, backupName);
|
|
fs.mkdirSync(backupPath, { recursive: true });
|
|
fs.writeFileSync(path.join(backupPath, 'manifest.json'), JSON.stringify({
|
|
files: ['console/.env'],
|
|
}));
|
|
|
|
try {
|
|
expect(() => restoreFromBackup(backupName))
|
|
.toThrow('Refusing to restore protected runtime path');
|
|
} finally {
|
|
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('refuses to remove protected runtime files from a backup manifest', () => {
|
|
const backupRoot = path.join(config.dataDir, 'backups');
|
|
const backupName = `pre-update-${Date.now()}-${process.pid}`;
|
|
const backupPath = path.join(backupRoot, backupName);
|
|
fs.mkdirSync(backupPath, { recursive: true });
|
|
fs.writeFileSync(path.join(backupPath, 'manifest.json'), JSON.stringify({
|
|
removeOnRestore: ['console/.env'],
|
|
}));
|
|
|
|
try {
|
|
expect(() => restoreFromBackup(backupName))
|
|
.toThrow('Refusing to remove protected runtime path');
|
|
} finally {
|
|
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|