Phase 1: Add ESLint/Prettier config + baseline auto-fixes
This commit is contained in:
@@ -150,7 +150,7 @@ describe('Sites Route Security', () => {
|
||||
.post('/api/site/external')
|
||||
.send({
|
||||
subdomain: 'test',
|
||||
externalUrl: 'https://evil.com/path{inject}'
|
||||
externalUrl: 'https://evil.com/path{inject}',
|
||||
});
|
||||
|
||||
// Should be rejected — either 400 (our validation) or 500 (URL constructor throws on {})
|
||||
@@ -164,7 +164,7 @@ describe('Sites Route Security', () => {
|
||||
.post('/api/site/external')
|
||||
.send({
|
||||
subdomain: 'test',
|
||||
externalUrl: 'https://evil.com/path\nreverse_proxy malicious:1234'
|
||||
externalUrl: 'https://evil.com/path\nreverse_proxy malicious:1234',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
@@ -183,7 +183,7 @@ describe('Sites Route Security', () => {
|
||||
.post('/api/site/external')
|
||||
.send({
|
||||
subdomain: '../etc/passwd',
|
||||
externalUrl: 'https://example.com'
|
||||
externalUrl: 'https://example.com',
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
@@ -205,7 +205,7 @@ describe('Error Logs — No Stack Trace Leak', () => {
|
||||
'[2026-03-07 12:01:00] dns: DNS timeout',
|
||||
'Error: connect ECONNREFUSED 192.168.1.1:5380',
|
||||
' at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1234:16)',
|
||||
'================================================================================'
|
||||
'================================================================================',
|
||||
].join('\n');
|
||||
// Write to the server's error log file location
|
||||
// The server uses ctx.ERROR_LOG_FILE — we need to check what that resolves to
|
||||
@@ -334,10 +334,10 @@ describe('Backup Security', () => {
|
||||
files: {
|
||||
encryptionKey: {
|
||||
type: 'text',
|
||||
content: 'malicious-key-data'
|
||||
}
|
||||
}
|
||||
}
|
||||
content: 'malicious-key-data',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// The encryptionKey should be skipped (not in fileMapping)
|
||||
@@ -392,8 +392,8 @@ describe('Custom Volume Path Validation', () => {
|
||||
port: '32400',
|
||||
customVolumes: [{
|
||||
containerPath: '/config',
|
||||
hostPath: '/etc/shadow'
|
||||
}]
|
||||
hostPath: '/etc/shadow',
|
||||
}],
|
||||
});
|
||||
|
||||
// The deploy will likely fail for other reasons (no Docker, etc.)
|
||||
@@ -414,7 +414,7 @@ describe('Logo Delete Path Traversal', () => {
|
||||
// Write config with a malicious logo path
|
||||
const configWithMaliciousLogo = {
|
||||
customLogo: '/assets/../../etc/passwd',
|
||||
customLogoDark: '/assets/../../../root/.ssh/id_rsa'
|
||||
customLogoDark: '/assets/../../../root/.ssh/id_rsa',
|
||||
};
|
||||
await fsp.writeFile(testConfigFile, JSON.stringify(configWithMaliciousLogo), 'utf8');
|
||||
|
||||
@@ -439,7 +439,7 @@ describe('DNS Server SSRF Prevention', () => {
|
||||
.query({
|
||||
domain: 'test.sami',
|
||||
type: 'A',
|
||||
server: '169.254.169.254' // AWS metadata endpoint
|
||||
server: '169.254.169.254', // AWS metadata endpoint
|
||||
});
|
||||
|
||||
// Must never succeed — 400 (server rejected), 401 (no token), or 500 (dns not configured in test)
|
||||
@@ -452,7 +452,7 @@ describe('DNS Server SSRF Prevention', () => {
|
||||
.send({
|
||||
domain: 'test.sami',
|
||||
ipAddress: '192.168.1.1',
|
||||
server: '10.0.0.1' // Not a configured DNS server
|
||||
server: '10.0.0.1', // Not a configured DNS server
|
||||
});
|
||||
|
||||
expect(res.statusCode).not.toBe(200);
|
||||
@@ -463,7 +463,7 @@ describe('DNS Server SSRF Prevention', () => {
|
||||
.get('/api/dns/resolve')
|
||||
.query({
|
||||
domain: 'test.sami',
|
||||
server: '127.0.0.1'
|
||||
server: '127.0.0.1',
|
||||
});
|
||||
|
||||
expect(res.statusCode).not.toBe(200);
|
||||
@@ -503,7 +503,7 @@ describe('HTTP Fetch Response Size Limit', () => {
|
||||
test('server should define MAX_RESPONSE_SIZE constant', () => {
|
||||
// Read server.js and verify the limit is defined
|
||||
const serverSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8',
|
||||
);
|
||||
expect(serverSource).toContain('MAX_RESPONSE_SIZE');
|
||||
expect(serverSource).toContain('10 * 1024 * 1024');
|
||||
@@ -516,7 +516,7 @@ describe('HTTP Fetch Response Size Limit', () => {
|
||||
describe('Middleware Security', () => {
|
||||
test('middleware should set Secure flag on cookies', () => {
|
||||
const middlewareSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'middleware.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'middleware.js'), 'utf8',
|
||||
);
|
||||
// Verify the Set-Cookie string includes Secure
|
||||
expect(middlewareSource).toContain('; Secure;');
|
||||
@@ -529,7 +529,7 @@ describe('Middleware Security', () => {
|
||||
describe('Config Save Atomicity', () => {
|
||||
test('saveConfig should use state manager for locking', () => {
|
||||
const serverSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8',
|
||||
);
|
||||
// Verify saveConfig uses configStateManager.update (not raw fs.writeFile)
|
||||
expect(serverSource).toContain('configStateManager.update');
|
||||
@@ -542,7 +542,7 @@ describe('Config Save Atomicity', () => {
|
||||
describe('External URL Security', () => {
|
||||
test('sites.js should validate URL components for unsafe chars', () => {
|
||||
const sitesSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'sites.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'sites.js'), 'utf8',
|
||||
);
|
||||
// Verify the unsafe character regex exists
|
||||
expect(sitesSource).toContain('unsafeCaddyChars');
|
||||
@@ -556,7 +556,7 @@ describe('External URL Security', () => {
|
||||
describe('Credential Manager File Locking', () => {
|
||||
test('credential-manager should use proper-lockfile', () => {
|
||||
const cmSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'credential-manager.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'credential-manager.js'), 'utf8',
|
||||
);
|
||||
expect(cmSource).toContain('proper-lockfile');
|
||||
expect(cmSource).toContain('_lockedUpdate');
|
||||
@@ -569,7 +569,7 @@ describe('Credential Manager File Locking', () => {
|
||||
describe('TOTP Config File Security', () => {
|
||||
test('loadTotpConfig should delete secret from file data', () => {
|
||||
const serverSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'server.js'), 'utf8',
|
||||
);
|
||||
// Verify the secret deletion exists in loadTotpConfig
|
||||
expect(serverSource).toContain('delete loaded.secret');
|
||||
@@ -577,7 +577,7 @@ describe('TOTP Config File Security', () => {
|
||||
|
||||
test('totp verify-setup should not write secret to config file', () => {
|
||||
const totpSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'auth', 'totp.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'auth', 'totp.js'), 'utf8',
|
||||
);
|
||||
// Verify totpConfig.secret assignment is NOT present
|
||||
expect(totpSource).not.toContain('totpConfig.secret = pendingSecret');
|
||||
@@ -591,7 +591,7 @@ describe('TOTP Config File Security', () => {
|
||||
describe('Helpers — Volume Security', () => {
|
||||
test('helpers.js should validate hostPath against allowed roots', () => {
|
||||
const helpersSource = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'apps', 'helpers.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'apps', 'helpers.js'), 'utf8',
|
||||
);
|
||||
expect(helpersSource).toContain('allowedRoots');
|
||||
expect(helpersSource).toContain('platformPaths.dockerData');
|
||||
@@ -605,7 +605,7 @@ describe('Helpers — Volume Security', () => {
|
||||
describe('Error Logs — Response Format', () => {
|
||||
test('errorlogs.js should not include details field', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'errorlogs.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'errorlogs.js'), 'utf8',
|
||||
);
|
||||
// The parsed log object should only have timestamp, context, error
|
||||
// NOT details (which contains stack traces)
|
||||
@@ -622,7 +622,7 @@ describe('Error Logs — Response Format', () => {
|
||||
describe('Assets — Logo Path Safety', () => {
|
||||
test('assets.js should use path.basename for logo filename extraction', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'config', 'assets.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'config', 'assets.js'), 'utf8',
|
||||
);
|
||||
expect(source).toContain('path.basename(logoPath)');
|
||||
// Should NOT use string replace for path extraction
|
||||
@@ -636,7 +636,7 @@ describe('Assets — Logo Path Safety', () => {
|
||||
describe('Backup — Encryption Key Exclusion', () => {
|
||||
test('backup.js should not include encryptionKey in filesToBackup', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8',
|
||||
);
|
||||
// Should have a comment about deliberate exclusion
|
||||
expect(source).toContain('encryptionKey deliberately excluded');
|
||||
@@ -646,7 +646,7 @@ describe('Backup — Encryption Key Exclusion', () => {
|
||||
|
||||
test('backup.js restore fileMapping should not include encryptionKey', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8',
|
||||
);
|
||||
// The RESTORE route's fileMapping (after "encryptionKey excluded" comment) must not have it
|
||||
// The preview route's fileMapping is allowed to have it (informational only)
|
||||
@@ -659,7 +659,7 @@ describe('Backup — Encryption Key Exclusion', () => {
|
||||
|
||||
test('backup.js should require TOTP for sensitive restores', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'config', 'backup.js'), 'utf8',
|
||||
);
|
||||
expect(source).toContain('sensitiveKeys');
|
||||
expect(source).toContain('totpCode');
|
||||
@@ -673,7 +673,7 @@ describe('Backup — Encryption Key Exclusion', () => {
|
||||
describe('DNS — Server Validation Function', () => {
|
||||
test('dns.js should define validateDnsServer', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'dns.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'dns.js'), 'utf8',
|
||||
);
|
||||
expect(source).toContain('function validateDnsServer');
|
||||
expect(source).toContain('configuredIps');
|
||||
@@ -687,7 +687,7 @@ describe('DNS — Server Validation Function', () => {
|
||||
describe('Containers — Verified Container Access', () => {
|
||||
test('containers.js update route should use getVerifiedContainer', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'containers.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'containers.js'), 'utf8',
|
||||
);
|
||||
// update and check-update should both use getVerifiedContainer
|
||||
const updateSection = source.substring(source.indexOf("'/:id/update'"));
|
||||
@@ -704,7 +704,7 @@ describe('Containers — Verified Container Access', () => {
|
||||
describe('Logs — Symlink Resolution', () => {
|
||||
test('logs.js should use realpath for symlink resolution', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'logs.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'logs.js'), 'utf8',
|
||||
);
|
||||
expect(source).toContain('fsp.realpath');
|
||||
expect(source).toContain('path.sep');
|
||||
@@ -712,7 +712,7 @@ describe('Logs — Symlink Resolution', () => {
|
||||
|
||||
test('logs.js container routes should verify container exists', () => {
|
||||
const source = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'routes', 'logs.js'), 'utf8'
|
||||
path.join(__dirname, '..', 'routes', 'logs.js'), 'utf8',
|
||||
);
|
||||
// Both container/:id and stream/:id should have inspect + NotFoundError
|
||||
expect(source).toContain('container.inspect()');
|
||||
|
||||
Reference in New Issue
Block a user