Files
BetterDesk/web-nodejs/tests/safePath.test.js
T
UNITRONIX c2e0e2e784 fix(security): harden CodeQL findings across console and Go server
Address GitHub code scanning alerts with OIDC SSRF guards, confined path
helpers, safer client routing, branding sanitization, upload rate limits,
and CodeQL config exclusions for dev-only and protocol-intentional hashes.
2026-06-11 06:57:58 +02:00

90 lines
3.2 KiB
JavaScript

'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
isPathInsideRoot,
resolveChildPath,
resolvePathWithinRoot,
resolvePathWithinAnyRoot,
resolveLangFilePath,
resolvePathUnderRoot,
readLangFileText,
langFileExists,
} = require('../lib/safePath');
describe('safePath', () => {
let tmpRoot;
beforeEach(() => {
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'bd-safe-path-'));
});
afterEach(() => {
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
test('isPathInsideRoot accepts descendants', () => {
const child = path.join(tmpRoot, 'a', 'b.txt');
expect(isPathInsideRoot(child, tmpRoot)).toBe(true);
});
test('isPathInsideRoot rejects parent escape', () => {
const outside = path.join(tmpRoot, '..', 'etc', 'passwd');
expect(isPathInsideRoot(outside, tmpRoot)).toBe(false);
});
test('resolveChildPath blocks traversal in segment', () => {
expect(() => resolveChildPath(tmpRoot, '../etc/passwd')).toThrow(/segment/i);
expect(() => resolveChildPath(tmpRoot, 'foo/bar')).toThrow(/segment/i);
});
test('resolveChildPath returns path under root', () => {
const p = resolveChildPath(tmpRoot, 'backup-1');
expect(p).toBe(path.resolve(tmpRoot, 'backup-1'));
});
test('resolvePathWithinRoot follows realpath for existing files', () => {
const sub = path.join(tmpRoot, 'sub');
fs.mkdirSync(sub);
const file = path.join(sub, 'f.txt');
fs.writeFileSync(file, 'ok');
const resolved = resolvePathWithinRoot(file, tmpRoot);
expect(resolved).toBe(fs.realpathSync.native(file));
});
test('resolvePathWithinAnyRoot allows configured roots', () => {
const p = resolvePathWithinAnyRoot(path.join(tmpRoot, 'x'), [tmpRoot, '/var/log']);
expect(p).toBe(path.resolve(tmpRoot, 'x'));
});
test('resolveLangFilePath confines to lang dir', () => {
const langDir = path.join(tmpRoot, 'lang');
fs.mkdirSync(langDir);
const p = resolveLangFilePath(langDir, 'en');
expect(p).toBe(path.join(langDir, 'en.json'));
expect(() => resolveLangFilePath(langDir, '../secrets')).toThrow();
});
test('resolvePathUnderRoot follows nested relative paths', () => {
const sub = path.join(tmpRoot, 'a', 'b');
fs.mkdirSync(sub, { recursive: true });
const p = resolvePathUnderRoot(tmpRoot, 'a/b/file.txt');
expect(p).toBe(path.join(sub, 'file.txt'));
});
test('resolvePathUnderRoot blocks traversal', () => {
expect(() => resolvePathUnderRoot(tmpRoot, '../etc/passwd')).toThrow();
expect(() => resolvePathUnderRoot(tmpRoot, 'a/../../etc/passwd')).toThrow();
});
test('readLangFileText reads confined language file', () => {
const langDir = path.join(tmpRoot, 'lang');
fs.mkdirSync(langDir, { recursive: true });
fs.writeFileSync(path.join(langDir, 'en.json'), '{"hello":"world"}');
expect(readLangFileText(langDir, 'en')).toBe('{"hello":"world"}');
expect(langFileExists(langDir, 'en')).toBe(true);
});
});