fix: harden local dashboard and data boundaries (#2585)

* fix: harden local data boundaries

Bind the capabilities dashboard exclusively to loopback and reject untrusted Host and Origin values. Constrain project-configured agent data paths to the Cursor data root, and harden lifecycle repair/uninstall operations against state-file traversal, symlink swaps, unsafe sources, and forged install-state destinations.\n\nCloses #2506

* fix: eliminate repair source read race

Read source bytes and mode from one no-follow file descriptor so a path replacement cannot mix metadata from one inode with content from another. Add a regression that rejects separate path-based source metadata lookup.

* fix: close dashboard hardening review gaps
This commit is contained in:
Affaan Mustafa 2026-07-27 11:11:29 -07:00 committed by GitHub
parent 4da6deac18
commit 382060905e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 2320 additions and 193 deletions

View file

@ -68,6 +68,20 @@ function withIsolatedCwd(fn) {
}
}
function captureConsoleErrors(fn) {
const originalError = console.error;
const messages = [];
console.error = (...args) => {
messages.push(args.join(' '));
};
try {
return { result: fn(), messages };
} finally {
console.error = originalError;
}
}
function runTests() {
console.log('\n=== Testing agent-data-home.js ===\n');
let passed = 0;
@ -148,10 +162,11 @@ function runTests() {
})) passed++; else failed++;
if (test('reads project ecc-agent-data.json config file', () => {
const tmpDir = path.join(os.tmpdir(), `ecc-agent-data-home-read-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true });
const configPath = path.join(tmpDir, 'ecc-agent-data.json');
const customHome = path.join(tmpDir, 'data-root');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-read-'));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-user-'));
const configPath = path.join(tmpDir, '.cursor', 'ecc-agent-data.json');
const customHome = path.join(homeDir, '.cursor', 'ecc', 'custom');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(
configPath,
JSON.stringify({ agentDataHome: customHome }),
@ -162,27 +177,74 @@ function runTests() {
withEnv({
ECC_AGENT_DATA_HOME: undefined,
CURSOR_VERSION: undefined,
HOME: homeDir,
USERPROFILE: undefined,
}, () => {
const agentDataHome = require('../../scripts/lib/agent-data-home');
assert.strictEqual(
agentDataHome.readProjectConfigAt(configPath),
path.resolve(customHome)
path.join(fs.realpathSync(homeDir), '.cursor', 'ecc', 'custom')
);
});
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('resolves relative agentDataHome against project root, not cwd', () => {
if (test('allows the documented ~/.claude project sharing root and its descendants', () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-claude-'));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-claude-user-'));
const configPath = path.join(projectDir, '.cursor', 'ecc-agent-data.json');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.mkdirSync(path.join(homeDir, '.claude'), { recursive: true });
try {
withEnv({
ECC_AGENT_DATA_HOME: undefined,
HOME: homeDir,
USERPROFILE: undefined,
}, () => {
const agentDataHome = require('../../scripts/lib/agent-data-home');
const cases = [
{
candidate: '~/.claude',
expected: path.join(fs.realpathSync(homeDir), '.claude'),
},
{
candidate: '~/.claude/shared',
expected: path.join(fs.realpathSync(homeDir), '.claude', 'shared'),
},
];
for (const { candidate, expected } of cases) {
fs.writeFileSync(
configPath,
JSON.stringify({ agentDataHome: candidate }),
'utf8'
);
assert.strictEqual(
agentDataHome.readProjectConfigAt(configPath),
expected
);
}
});
} finally {
fs.rmSync(projectDir, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('rejects a relative agentDataHome that redirects into the project', () => {
const stamp = Date.now();
const projectDir = path.join(os.tmpdir(), `ecc-agent-data-home-relative-${stamp}`);
const cursorDir = path.join(projectDir, '.cursor');
const otherCwd = path.join(os.tmpdir(), `ecc-agent-data-home-other-cwd-${stamp}`);
const homeDir = path.join(os.tmpdir(), `ecc-agent-data-home-relative-user-${stamp}`);
fs.mkdirSync(cursorDir, { recursive: true });
fs.mkdirSync(otherCwd, { recursive: true });
fs.mkdirSync(homeDir, { recursive: true });
const configPath = path.join(cursorDir, 'ecc-agent-data.json');
const expectedHome = path.join(projectDir, '.ecc-data');
fs.writeFileSync(
configPath,
JSON.stringify({ agentDataHome: '.ecc-data' }),
@ -196,18 +258,200 @@ function runTests() {
ECC_AGENT_DATA_HOME: undefined,
CURSOR_VERSION: undefined,
CURSOR_PROJECT_DIR: projectDir,
HOME: homeDir,
USERPROFILE: undefined,
}, () => {
const agentDataHome = require('../../scripts/lib/agent-data-home');
assert.strictEqual(agentDataHome.readProjectConfigAt(configPath), expectedHome);
const { result, messages } = captureConsoleErrors(
() => agentDataHome.readProjectConfigAt(configPath)
);
assert.strictEqual(result, null);
assert.ok(messages.some(message => message.includes('Ignoring unsafe agent data project config')));
assert.ok(messages.every(message => !message.includes('.ecc-data')));
assert.strictEqual(
agentDataHome.resolveAgentDataHome({ projectDir }),
expectedHome
captureConsoleErrors(
() => agentDataHome.resolveAgentDataHome({ projectDir, preferCursorDefault: true })
).result,
path.join(homeDir, '.cursor', 'ecc')
);
});
} finally {
process.chdir(originalCwd);
fs.rmSync(projectDir, { recursive: true, force: true });
fs.rmSync(otherCwd, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('rejects relative project config paths even when the project is beneath the trusted root', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-nested-user-'));
const projectDir = path.join(homeDir, '.cursor', 'ecc', 'checked-out-project');
const configPath = path.join(projectDir, '.cursor', 'ecc-agent-data.json');
const candidate = '.repo-data';
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, JSON.stringify({ agentDataHome: candidate }), 'utf8');
try {
withEnv({
ECC_AGENT_DATA_HOME: undefined,
HOME: homeDir,
USERPROFILE: undefined,
}, () => {
const agentDataHome = require('../../scripts/lib/agent-data-home');
const { result, messages } = captureConsoleErrors(
() => agentDataHome.readProjectConfigAt(configPath)
);
assert.strictEqual(result, null);
assert.ok(messages.some(message => message.includes('Ignoring unsafe agent data project config')));
assert.ok(messages.every(message => !message.includes(candidate)));
});
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('rejects traversal and absolute project config paths outside the allowed data roots', () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-unsafe-'));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-unsafe-user-'));
const configPath = path.join(projectDir, '.cursor', 'ecc-agent-data.json');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
try {
withEnv({
ECC_AGENT_DATA_HOME: undefined,
HOME: homeDir,
USERPROFILE: undefined,
}, () => {
const agentDataHome = require('../../scripts/lib/agent-data-home');
const unsafeCandidates = [
'../../repo-data',
path.join(projectDir, 'absolute-data'),
'~/.cursor/ecc/profiles/../traversed-data',
'~/.claude/profiles/../traversed-data',
'~/.claude-other',
'~/.config/ecc',
'~',
path.join(homeDir, 'arbitrary-agent-data'),
];
for (const candidate of unsafeCandidates) {
fs.writeFileSync(configPath, JSON.stringify({ agentDataHome: candidate }), 'utf8');
const { result, messages } = captureConsoleErrors(
() => agentDataHome.readProjectConfigAt(configPath)
);
assert.strictEqual(result, null);
assert.ok(messages.some(message => message.includes('Ignoring unsafe agent data project config')));
assert.ok(messages.every(message => !message.includes(candidate)));
}
});
} finally {
fs.rmSync(projectDir, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('rejects a Claude project config destination that escapes through a symlink', () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-claude-link-'));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-claude-link-user-'));
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-claude-link-outside-'));
const configPath = path.join(projectDir, '.cursor', 'ecc-agent-data.json');
const claudeRoot = path.join(homeDir, '.claude');
const linkPath = path.join(claudeRoot, 'redirect');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.mkdirSync(claudeRoot, { recursive: true });
try {
try {
fs.symlinkSync(outsideDir, linkPath, 'dir');
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
return;
}
const candidate = path.join(linkPath, 'session-data');
fs.writeFileSync(configPath, JSON.stringify({ agentDataHome: candidate }), 'utf8');
withEnv({
ECC_AGENT_DATA_HOME: undefined,
HOME: homeDir,
USERPROFILE: undefined,
}, () => {
const agentDataHome = require('../../scripts/lib/agent-data-home');
const { result, messages } = captureConsoleErrors(
() => agentDataHome.readProjectConfigAt(configPath)
);
assert.strictEqual(result, null);
assert.ok(messages.some(message => message.includes('Ignoring unsafe agent data project config')));
assert.ok(messages.every(message => !message.includes(candidate)));
});
} finally {
fs.rmSync(projectDir, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(outsideDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('allows a non-existent project config destination beneath the Cursor data root', () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-safe-'));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-safe-user-'));
const configPath = path.join(projectDir, '.cursor', 'ecc-agent-data.json');
const safeHome = path.join(homeDir, '.cursor', 'ecc', 'profiles', 'work');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, JSON.stringify({ agentDataHome: safeHome }), 'utf8');
try {
withEnv({
ECC_AGENT_DATA_HOME: undefined,
HOME: homeDir,
USERPROFILE: undefined,
}, () => {
const agentDataHome = require('../../scripts/lib/agent-data-home');
assert.strictEqual(
agentDataHome.readProjectConfigAt(configPath),
path.join(fs.realpathSync(homeDir), '.cursor', 'ecc', 'profiles', 'work')
);
});
} finally {
fs.rmSync(projectDir, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
}
})) passed++; else failed++;
if (test('rejects a project config destination that escapes through a symlink', () => {
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-link-'));
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-link-user-'));
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-agent-data-home-link-outside-'));
const configPath = path.join(projectDir, '.cursor', 'ecc-agent-data.json');
const cursorRoot = path.join(homeDir, '.cursor', 'ecc');
const linkPath = path.join(cursorRoot, 'redirect');
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.mkdirSync(cursorRoot, { recursive: true });
try {
try {
fs.symlinkSync(outsideDir, linkPath, 'dir');
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
return;
}
const candidate = path.join(linkPath, 'session-data');
fs.writeFileSync(configPath, JSON.stringify({ agentDataHome: candidate }), 'utf8');
withEnv({
ECC_AGENT_DATA_HOME: undefined,
HOME: homeDir,
USERPROFILE: undefined,
}, () => {
const agentDataHome = require('../../scripts/lib/agent-data-home');
const { result, messages } = captureConsoleErrors(
() => agentDataHome.readProjectConfigAt(configPath)
);
assert.strictEqual(result, null);
assert.ok(messages.some(message => message.includes('Ignoring unsafe agent data project config')));
assert.ok(messages.every(message => !message.includes(candidate)));
});
} finally {
fs.rmSync(projectDir, { recursive: true, force: true });
fs.rmSync(homeDir, { recursive: true, force: true });
fs.rmSync(outsideDir, { recursive: true, force: true });
}
})) passed++; else failed++;

View file

@ -17,6 +17,7 @@ const {
const { getInstallTargetAdapter } = require('../../scripts/lib/install-targets/registry');
const {
createInstallState,
readInstallState,
writeInstallState,
} = require('../../scripts/lib/install-state');
@ -731,6 +732,93 @@ function runTests() {
}
})) passed++; else failed++;
if (test('Claude repair migration derives roots from the adapter and removes only the managed legacy file', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
try {
const targetRoot = path.join(homeDir, '.claude');
const adapterStatePath = path.join(targetRoot, 'ecc', 'install-state.json');
const recordedStatePath = path.join(outsideRoot, 'recorded-state.json');
const flatSkillPath = path.join(targetRoot, 'skills', 'tdd-workflow', 'SKILL.md');
const legacySkillPath = path.join(
targetRoot,
'skills',
'ecc',
'tdd-workflow',
'SKILL.md'
);
fs.mkdirSync(path.dirname(legacySkillPath), { recursive: true });
fs.writeFileSync(legacySkillPath, '# Previously managed nested skill\n');
writeState(adapterStatePath, {
adapter: { id: 'claude-home', target: 'claude', kind: 'home' },
targetRoot: outsideRoot,
installStatePath: recordedStatePath,
request: {
profile: null,
modules: ['workflow-quality'],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['platform-configs', 'workflow-quality'],
skippedModules: [],
},
operations: [{
kind: 'copy-file',
moduleId: 'workflow-quality',
sourcePath: path.join(REPO_ROOT, 'skills', 'tdd-workflow', 'SKILL.md'),
sourceRelativePath: path.join('skills', 'tdd-workflow', 'SKILL.md'),
destinationPath: legacySkillPath,
strategy: 'preserve-relative-path',
ownership: 'managed',
scaffoldOnly: false,
}],
source: {
repoVersion: CURRENT_PACKAGE_VERSION,
repoCommit: 'abc123',
manifestVersion: CURRENT_MANIFEST_VERSION,
},
});
fs.writeFileSync(recordedStatePath, 'outside sentinel\n');
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['claude'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.strictEqual(
fs.readFileSync(flatSkillPath, 'utf8'),
fs.readFileSync(
path.join(REPO_ROOT, 'skills', 'tdd-workflow', 'SKILL.md'),
'utf8'
)
);
assert.ok(!fs.existsSync(legacySkillPath));
assert.strictEqual(fs.readFileSync(recordedStatePath, 'utf8'), 'outside sentinel\n');
const refreshedState = readInstallState(adapterStatePath);
assert.strictEqual(refreshedState.target.root, targetRoot);
assert.strictEqual(refreshedState.target.installStatePath, adapterStatePath);
assert.ok(refreshedState.operations.some(operation => (
operation.destinationPath === flatSkillPath
)));
assert.ok(!refreshedState.operations.some(operation => (
operation.destinationPath === legacySkillPath
)));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('repair copies missing managed files from recorded source paths', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
@ -763,6 +851,47 @@ function runTests() {
}
})) passed++; else failed++;
if (test('repair reads source content and mode from one no-follow descriptor', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const sourcePath = path.join(REPO_ROOT, 'rules', 'common', 'coding-style.md');
const originalStatSync = fs.statSync;
try {
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'rules', 'coding-style.md');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, {
sourceRelativePath: 'rules/common/coding-style.md',
strategy: 'copy-file',
}),
],
});
fs.statSync = function rejectSeparateSourceMetadataLookup(candidatePath, ...args) {
if (path.resolve(candidatePath) === path.resolve(sourcePath)) {
throw new Error('source metadata must come from the opened descriptor');
}
return originalStatSync.call(fs, candidatePath, ...args);
};
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'repaired');
assert.ok(fs.readFileSync(destinationPath).equals(fs.readFileSync(sourcePath)));
} finally {
fs.statSync = originalStatSync;
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair reports invalid states, missing sources, unsupported operations, and no-op refreshes', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const invalidProjectRoot = createTempDir('install-lifecycle-invalid-');
@ -1114,6 +1243,162 @@ function runTests() {
}
})) passed++; else failed++;
if (test('repair rejects absolute and parent-relative source metadata outside the repository', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const outsideRoot = createTempDir('install-lifecycle-source-outside-');
const outsideSourcePath = path.join(outsideRoot, 'secret.txt');
fs.writeFileSync(outsideSourcePath, 'outside secret\n');
try {
const unsafeSources = [
outsideSourcePath,
path.relative(REPO_ROOT, outsideSourcePath),
];
for (const sourceRelativePath of unsafeSources) {
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const destinationPath = path.join(projectRoot, '.cursor', 'copied-secret.txt');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, {
sourceRelativePath,
strategy: 'copy-file',
}),
],
});
const doctor = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(doctor.results[0].status, 'error');
assert.ok(
doctor.results[0].issues.some(
issue => issue.code === 'unsafe-repair-source'
)
);
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('unsafe repair source metadata'));
assert.ok(!result.results[0].error.includes(outsideSourcePath));
assert.ok(!fs.existsSync(destinationPath));
} finally {
cleanup(projectRoot);
}
}
} finally {
cleanup(homeDir);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('doctor and repair reject unsafe destinations before health inspection reads them', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const outsideRoot = createTempDir('install-lifecycle-destination-outside-');
const copySource = fs.readFileSync(
path.join(REPO_ROOT, 'rules', 'common', 'coding-style.md'),
'utf8'
);
const cases = [
{
name: 'matching copy',
kind: 'copy-file',
content: copySource,
overrides: { strategy: 'copy-file' },
},
{
name: 'drifted copy',
kind: 'copy-file',
content: 'outside drift\n',
overrides: { strategy: 'copy-file' },
},
{
name: 'rendered template',
kind: 'render-template',
content: 'managed template\n',
overrides: {
renderedContent: 'managed template\n',
strategy: 'render-template',
},
},
{
name: 'merged JSON',
kind: 'merge-json',
content: '{"managed":true,"outside":"sentinel"}\n',
overrides: {
mergePayload: { managed: true },
strategy: 'merge-json',
},
},
];
try {
for (const testCase of cases) {
const projectRoot = createTempDir('install-lifecycle-project-');
const destinationPath = path.join(outsideRoot, `${testCase.name}.txt`);
const originalExistsSync = fs.existsSync;
try {
fs.writeFileSync(destinationPath, testCase.content);
writeCursorState(projectRoot, {
operations: [
managedOperation(testCase.kind, destinationPath, testCase.overrides),
],
});
fs.existsSync = function existsSyncWithoutOutsideInspection(candidatePath) {
if (path.resolve(candidatePath) === path.resolve(destinationPath)) {
throw new Error(`unsafe destination inspected: ${testCase.name}`);
}
return originalExistsSync.call(fs, candidatePath);
};
const doctor = buildDoctorReport({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
const repair = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(doctor.results[0].status, 'error');
assert.ok(
doctor.results[0].issues.some(
issue => issue.code === 'unsafe-managed-destination'
)
);
assert.strictEqual(repair.results[0].status, 'error');
assert.ok(repair.results[0].error.includes('unsafe managed destination'));
assert.strictEqual(
originalExistsSync.call(fs, destinationPath),
true,
`${testCase.name} destination should remain untouched`
);
} finally {
fs.existsSync = originalExistsSync;
cleanup(projectRoot);
}
}
} finally {
cleanup(homeDir);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('doctor reports drifted managed files as a warning', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
@ -1414,6 +1699,293 @@ function runTests() {
}
})) passed++; else failed++;
if (test('repair rejects a symlink inserted while creating a missing destination parent', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
const targetRoot = path.join(projectRoot, '.cursor');
const destinationParent = path.join(targetRoot, 'late-parent');
const destinationPath = path.join(destinationParent, 'managed.md');
const outsideDestinationPath = path.join(outsideRoot, 'managed.md');
const originalMkdirSync = fs.mkdirSync;
let canonicalDestinationParent;
let insertedSymlink = false;
let result;
try {
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
canonicalDestinationParent = path.join(
fs.realpathSync(targetRoot),
path.basename(destinationParent)
);
fs.mkdirSync = function mkdirSyncWithLateSymlink(directoryPath, options) {
if (!insertedSymlink && path.resolve(directoryPath) === canonicalDestinationParent) {
originalMkdirSync.call(fs, path.dirname(canonicalDestinationParent), { recursive: true });
fs.symlinkSync(
outsideRoot,
canonicalDestinationParent,
process.platform === 'win32' ? 'junction' : 'dir'
);
insertedSymlink = true;
return undefined;
}
return originalMkdirSync.call(fs, directoryPath, options);
};
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
} finally {
fs.mkdirSync = originalMkdirSync;
}
try {
assert.strictEqual(insertedSymlink, true);
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('outside the install root'));
assert.ok(!fs.existsSync(outsideDestinationPath));
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('repair rejects an in-root final symlink without overwriting its victim', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const victimPath = path.join(targetRoot, 'victim.md');
const destinationPath = path.join(targetRoot, 'managed.md');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(victimPath, 'victim sentinel\n');
try {
fs.symlinkSync(victimPath, destinationPath);
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
return;
}
writeCursorState(projectRoot, {
operations: [
managedOperation('render-template', destinationPath, {
renderedContent: 'managed replacement\n',
strategy: 'render-template',
}),
],
});
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('final symlink'));
assert.strictEqual(fs.readFileSync(victimPath, 'utf8'), 'victim sentinel\n');
assert.strictEqual(fs.lstatSync(destinationPath).isSymbolicLink(), true);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('repair uses no-follow writes when a final destination becomes a symlink', () => {
if (!fs.constants.O_NOFOLLOW) {
console.log(' (O_NOFOLLOW unsupported on this platform; skipping)');
return;
}
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
const targetRoot = path.join(projectRoot, '.cursor');
const destinationPath = path.join(targetRoot, 'managed.md');
const outsideDestinationPath = path.join(outsideRoot, 'managed.md');
const originalOpenSync = fs.openSync;
let canonicalDestinationPath;
let insertedSymlink = false;
let result;
try {
fs.writeFileSync(outsideDestinationPath, 'outside sentinel\n');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
canonicalDestinationPath = path.join(
fs.realpathSync(targetRoot),
path.basename(destinationPath)
);
fs.openSync = function openSyncWithLateSymlink(filePath, flags, mode) {
if (!insertedSymlink && path.resolve(filePath) === canonicalDestinationPath) {
fs.symlinkSync(outsideDestinationPath, canonicalDestinationPath);
insertedSymlink = true;
}
return originalOpenSync.call(fs, filePath, flags, mode);
};
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
} finally {
fs.openSync = originalOpenSync;
}
try {
assert.strictEqual(insertedSymlink, true);
assert.strictEqual(result.results[0].status, 'error');
assert.strictEqual(
fs.readFileSync(outsideDestinationPath, 'utf8'),
'outside sentinel\n'
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('repair revalidates a pinned write before a swapped parent can truncate outside files', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
const targetRoot = path.join(projectRoot, '.cursor');
const destinationParent = path.join(targetRoot, 'late-parent');
const backupParent = path.join(targetRoot, 'late-parent-backup');
const destinationPath = path.join(destinationParent, 'managed.md');
const outsideDestinationPath = path.join(outsideRoot, 'managed.md');
const originalOpenSync = fs.openSync;
let canonicalDestinationPath;
let insertedSymlink = false;
let result;
const symlinkProbe = path.join(targetRoot, 'parent-symlink-probe');
try {
fs.mkdirSync(targetRoot, { recursive: true });
fs.symlinkSync(
outsideRoot,
symlinkProbe,
process.platform === 'win32' ? 'junction' : 'dir'
);
fs.rmSync(symlinkProbe, { force: true });
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
return;
}
try {
fs.mkdirSync(destinationParent, { recursive: true });
fs.writeFileSync(destinationPath, 'drifted managed content\n');
fs.writeFileSync(outsideDestinationPath, 'outside sentinel\n');
canonicalDestinationPath = fs.realpathSync(destinationPath);
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
fs.openSync = function openSyncWithLateParentSwap(filePath, flags, mode) {
const isDestinationWrite = path.resolve(filePath) === canonicalDestinationPath
&& typeof flags === 'number'
&& (flags & fs.constants.O_WRONLY) === fs.constants.O_WRONLY;
if (!insertedSymlink && isDestinationWrite) {
fs.renameSync(destinationParent, backupParent);
fs.symlinkSync(
outsideRoot,
destinationParent,
process.platform === 'win32' ? 'junction' : 'dir'
);
insertedSymlink = true;
}
return originalOpenSync.call(fs, filePath, flags, mode);
};
result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
} finally {
fs.openSync = originalOpenSync;
}
try {
assert.strictEqual(insertedSymlink, true);
assert.strictEqual(result.results[0].status, 'error');
assert.strictEqual(
fs.readFileSync(outsideDestinationPath, 'utf8'),
'outside sentinel\n'
);
assert.strictEqual(
fs.readFileSync(path.join(backupParent, 'managed.md'), 'utf8'),
'drifted managed content\n'
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('repair refreshes only the adapter-derived install-state path', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const adapterStatePath = path.join(targetRoot, 'ecc-install-state.json');
const recordedStatePath = path.join(outsideRoot, 'recorded-state.json');
const stateOptions = createCursorStateOptions(projectRoot, {
installStatePath: recordedStatePath,
});
writeState(adapterStatePath, stateOptions);
fs.writeFileSync(recordedStatePath, 'outside sentinel\n');
const result = repairInstalledStates({
repoRoot: REPO_ROOT,
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'ok');
assert.ok(fs.existsSync(adapterStatePath));
assert.strictEqual(
fs.readFileSync(recordedStatePath, 'utf8'),
'outside sentinel\n'
);
const refreshedState = readInstallState(adapterStatePath);
assert.strictEqual(refreshedState.target.root, targetRoot);
assert.strictEqual(refreshedState.target.installStatePath, adapterStatePath);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('uninstall restores JSON merged files from recorded previous content', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
@ -1662,6 +2234,40 @@ function runTests() {
}
})) passed++; else failed++;
if (test('uninstall removes only the adapter-derived install-state path', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const adapterStatePath = path.join(targetRoot, 'ecc-install-state.json');
const recordedStatePath = path.join(outsideRoot, 'recorded-state.json');
const stateOptions = createCursorStateOptions(projectRoot, {
installStatePath: recordedStatePath,
});
writeState(adapterStatePath, stateOptions);
fs.writeFileSync(recordedStatePath, 'outside sentinel\n');
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(!fs.existsSync(adapterStatePath));
assert.strictEqual(
fs.readFileSync(recordedStatePath, 'utf8'),
'outside sentinel\n'
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('uninstall removes copied files and cleans empty parent directories', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
@ -1694,6 +2300,42 @@ function runTests() {
}
})) passed++; else failed++;
if (test('uninstall cleanup stops at the adapter-derived target root', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const cleanupBoundaryRoot = createTempDir('install-lifecycle-boundary-');
const projectRoot = path.join(cleanupBoundaryRoot, 'project');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const adapterStatePath = path.join(targetRoot, 'ecc-install-state.json');
const destinationPath = path.join(targetRoot, 'rules', 'nested', 'managed.md');
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, 'managed\n');
const stateOptions = createCursorStateOptions(projectRoot, {
targetRoot: cleanupBoundaryRoot,
installStatePath: adapterStatePath,
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
writeState(adapterStatePath, stateOptions);
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(fs.existsSync(projectRoot));
assert.ok(fs.existsSync(targetRoot));
assert.ok(!fs.existsSync(destinationPath));
} finally {
cleanup(homeDir);
cleanup(cleanupBoundaryRoot);
}
})) passed++; else failed++;
if (test('uninstall handles merge-json subset removal and full-file deletion', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const partialProjectRoot = createTempDir('install-lifecycle-partial-');
@ -1899,6 +2541,107 @@ function runTests() {
}
})) passed++; else failed++;
if (test('uninstall removes an in-root final symlink without deleting its victim', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
try {
const targetRoot = path.join(projectRoot, '.cursor');
const victimPath = path.join(targetRoot, 'victim.md');
const destinationPath = path.join(targetRoot, 'managed.md');
fs.mkdirSync(targetRoot, { recursive: true });
fs.writeFileSync(victimPath, 'victim sentinel\n');
try {
fs.symlinkSync(victimPath, destinationPath);
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
return;
}
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
const result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
assert.strictEqual(result.results[0].status, 'uninstalled');
assert.ok(!fs.existsSync(destinationPath));
assert.strictEqual(fs.readFileSync(victimPath, 'utf8'), 'victim sentinel\n');
} finally {
cleanup(homeDir);
cleanup(projectRoot);
}
})) passed++; else failed++;
if (test('uninstall rejects a symlink inserted after initial destination validation', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');
const outsideRoot = createTempDir('install-lifecycle-outside-');
const targetRoot = path.join(projectRoot, '.cursor');
const destinationParent = path.join(targetRoot, 'late-parent');
const backupParent = path.join(targetRoot, 'late-parent-backup');
const destinationPath = path.join(destinationParent, 'managed.md');
const outsideDestinationPath = path.join(outsideRoot, 'managed.md');
const originalExistsSync = fs.existsSync;
let canonicalDestinationParent;
let canonicalDestinationPath;
let insertedSymlink = false;
let result;
try {
fs.mkdirSync(destinationParent, { recursive: true });
fs.writeFileSync(destinationPath, 'managed\n');
fs.writeFileSync(outsideDestinationPath, 'outside sentinel\n');
writeCursorState(projectRoot, {
operations: [
managedOperation('copy-file', destinationPath, { strategy: 'copy-file' }),
],
});
canonicalDestinationPath = fs.realpathSync(destinationPath);
canonicalDestinationParent = path.dirname(canonicalDestinationPath);
fs.existsSync = function existsSyncWithLateSymlink(candidatePath) {
if (!insertedSymlink && path.resolve(candidatePath) === canonicalDestinationPath) {
fs.renameSync(canonicalDestinationParent, backupParent);
fs.symlinkSync(
outsideRoot,
canonicalDestinationParent,
process.platform === 'win32' ? 'junction' : 'dir'
);
insertedSymlink = true;
}
return originalExistsSync.call(fs, candidatePath);
};
result = uninstallInstalledStates({
homeDir,
projectRoot,
targets: ['cursor'],
});
} finally {
fs.existsSync = originalExistsSync;
}
try {
assert.strictEqual(insertedSymlink, true);
assert.strictEqual(result.results[0].status, 'error');
assert.ok(result.results[0].error.includes('outside the install root'));
assert.strictEqual(
fs.readFileSync(outsideDestinationPath, 'utf8'),
'outside sentinel\n'
);
} finally {
cleanup(homeDir);
cleanup(projectRoot);
cleanup(outsideRoot);
}
})) passed++; else failed++;
if (test('uninstall restores previous JSON snapshots for template and remove operations', () => {
const homeDir = createTempDir('install-lifecycle-home-');
const projectRoot = createTempDir('install-lifecycle-project-');

View file

@ -60,6 +60,13 @@ function runTests() {
assert.strictEqual(parseHostHeader('bad:host:extra'), null);
})) passed++; else failed++;
if (test('rejects ports outside the valid TCP range', () => {
assert.strictEqual(parseHostHeader('localhost:65536'), null);
assert.strictEqual(parseHostHeader('localhost:99999'), null);
assert.strictEqual(parseHostHeader('[::1]:65536'), null);
assert.strictEqual(parseHostHeader('localhost:65535'), 'localhost');
})) passed++; else failed++;
console.log('\nbuildAllowedHostnames:');
if (test('always includes loopback names', () => {

View file

@ -10,7 +10,11 @@ const fs = require('fs');
const os = require('os');
const path = require('path');
const { assertWithinTrustedRoot, isWithinRoot } = require('../../scripts/lib/path-safety');
const {
assertWithinTrustedRoot,
isWithinRoot,
realpathNearestExisting
} = require('../../scripts/lib/path-safety');
let passed = 0;
let failed = 0;
@ -43,12 +47,89 @@ try {
assert.strictEqual(isWithinRoot(root, root), true);
});
test('allows a non-existent destination beneath a non-existent trusted root', () => {
const futureRoot = path.join(root, 'future-root');
const futureDestination = path.join(futureRoot, 'session-data', 'session.json');
assert.strictEqual(isWithinRoot(futureDestination, futureRoot), true);
assert.strictEqual(
assertWithinTrustedRoot(futureDestination, futureRoot, 'write'),
realpathNearestExisting(futureDestination)
);
});
test('canonicalizes the nearest existing ancestor for a non-existent trusted root', () => {
const realParent = fs.mkdtempSync(path.join(os.tmpdir(), 'path-safety-real-'));
const linkedParent = path.join(
os.tmpdir(),
`path-safety-link-${process.pid}-${Date.now()}`
);
try {
fs.symlinkSync(realParent, linkedParent, 'dir');
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
fs.rmSync(realParent, { recursive: true, force: true });
return;
}
try {
const futureRoot = path.join(linkedParent, '.cursor', 'ecc');
const futureDestination = path.join(futureRoot, 'session-data', 'session.json');
assert.strictEqual(isWithinRoot(futureDestination, futureRoot), true);
assert.strictEqual(
assertWithinTrustedRoot(futureDestination, futureRoot, 'write'),
path.join(
fs.realpathSync(realParent),
'.cursor',
'ecc',
'session-data',
'session.json'
)
);
} finally {
fs.rmSync(linkedParent, { force: true });
fs.rmSync(realParent, { recursive: true, force: true });
}
});
test('returns the same canonical destination that was checked for containment', () => {
const destination = path.join(root, 'single-canonicalization.txt');
const originalRealpathSync = fs.realpathSync;
let destinationCanonicalizations = 0;
fs.writeFileSync(destination, 'safe\n');
fs.realpathSync = function countedRealpathSync(candidatePath, options) {
if (path.resolve(candidatePath) === path.resolve(destination)) {
destinationCanonicalizations += 1;
}
return originalRealpathSync.call(fs, candidatePath, options);
};
try {
assert.strictEqual(
assertWithinTrustedRoot(destination, root, 'write'),
originalRealpathSync(destination)
);
} finally {
fs.realpathSync = originalRealpathSync;
}
assert.strictEqual(destinationCanonicalizations, 1);
});
test('refuses an absolute path outside the root', () => {
const evil = path.join(outside, 'PWNED.txt');
assert.throws(() => assertWithinTrustedRoot(evil, root, 'repair'), /outside the install root/);
assert.strictEqual(isWithinRoot(evil, root), false);
});
test('refuses an escape from a non-existent trusted root', () => {
const futureRoot = path.join(root, 'future-root');
const evil = path.join(futureRoot, '..', 'escape.txt');
assert.throws(() => assertWithinTrustedRoot(evil, futureRoot, 'write'), /outside the install root/);
assert.strictEqual(isWithinRoot(evil, futureRoot), false);
});
test('refuses a ../ traversal escape', () => {
const evil = path.join(root, '..', 'escape.txt');
assert.throws(() => assertWithinTrustedRoot(evil, root, 'uninstall'), /outside the install root/);
@ -67,6 +148,23 @@ try {
assert.throws(() => assertWithinTrustedRoot(evil, root, 'repair'), /outside the install root/);
});
test('refuses a dangling symlinked intermediate directory', () => {
const danglingTarget = path.join(outside, 'missing-target');
const linkDir = path.join(root, 'dangling-link');
try {
fs.symlinkSync(danglingTarget, linkDir, 'dir');
} catch {
console.log(' (symlink unsupported on this platform; skipping)');
return;
}
const evil = path.join(linkDir, 'session-data', 'session.json');
assert.strictEqual(isWithinRoot(evil, root), false);
assert.throws(
() => assertWithinTrustedRoot(evil, root, 'write'),
/outside the install root/
);
});
test('refuses when no trusted root is resolved', () => {
assert.throws(() => assertWithinTrustedRoot(path.join(root, 'x'), null, 'repair'), /no trusted install root/);
});

View file

@ -7,12 +7,15 @@ const fs = require('fs');
const os = require('os');
const path = require('path');
const http = require('http');
const net = require('net');
const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'dashboard-web.js');
let testRoot;
let testPassed = 0;
let testFailed = 0;
const asyncTests = [];
const REQUEST_TIMEOUT_MS = 5000;
function test(name, fn) {
try {
@ -28,6 +31,10 @@ function test(name, fn) {
}
}
function asyncTest(name, fn) {
asyncTests.push({ name, fn });
}
function createTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
@ -65,6 +72,121 @@ function writeFile(rootDir, relativePath, content) {
fs.writeFileSync(targetPath, content);
}
function requestDashboard(port, options = {}) {
return new Promise((resolve, reject) => {
let settled = false;
let request;
const settle = (callback, value) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
callback(value);
};
const timeout = setTimeout(() => {
const error = new Error(
`Dashboard request timed out after ${REQUEST_TIMEOUT_MS}ms`
);
if (request) request.destroy();
settle(reject, error);
}, REQUEST_TIMEOUT_MS);
request = http.request({
host: '127.0.0.1',
port,
method: options.method || 'GET',
path: options.path || '/',
headers: options.headers || {},
setHost: options.setHost !== false,
}, (response) => {
let body = '';
response.setEncoding('utf8');
response.on('data', (chunk) => {
body += chunk;
});
response.on('error', error => settle(reject, error));
response.on('end', () => {
settle(resolve, {
body,
headers: response.headers,
statusCode: response.statusCode,
});
});
});
request.on('error', error => settle(reject, error));
request.end();
});
}
function requestDashboardWithoutHost(port) {
return new Promise((resolve, reject) => {
const socket = net.createConnection({ host: '127.0.0.1', port });
let raw = '';
let settled = false;
const settle = (callback, value) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
callback(value);
};
const timeout = setTimeout(() => {
const error = new Error(
`Host-less request timed out after ${REQUEST_TIMEOUT_MS}ms`
);
socket.destroy();
settle(reject, error);
}, REQUEST_TIMEOUT_MS);
socket.setEncoding('utf8');
socket.on('connect', () => {
socket.write('GET / HTTP/1.0\r\n\r\n');
});
socket.on('data', (chunk) => {
raw += chunk;
});
socket.on('end', () => {
const [head, body = ''] = raw.split('\r\n\r\n');
const lines = head.split('\r\n');
const statusCode = Number.parseInt(lines[0].split(' ')[1], 10);
const headers = {};
for (const line of lines.slice(1)) {
const separator = line.indexOf(':');
if (separator < 1) continue;
headers[line.slice(0, separator).toLowerCase()] = line.slice(separator + 1).trim();
}
settle(resolve, { body, headers, statusCode });
});
socket.on('error', error => settle(reject, error));
socket.on('close', hadError => {
if (!hadError && !settled) {
settle(reject, new Error('Host-less request closed before completion'));
}
});
});
}
async function withDashboardServer(fn, serverOptions = {}) {
const { createDashboardServer } = require(SCRIPT);
const testServer = createDashboardServer({
host: '127.0.0.1',
...serverOptions,
});
await new Promise((resolve, reject) => {
testServer.once('error', reject);
testServer.listen(0, '127.0.0.1', () => {
testServer.off('error', reject);
resolve();
});
});
try {
await fn(testServer.address().port);
} finally {
await new Promise((resolve, reject) => {
testServer.close(error => (error ? reject(error) : resolve()));
});
}
}
// ===================== parsePort =====================
test('parsePort returns 3456 for undefined', () => {
@ -721,49 +843,228 @@ test('renderHTML includes the dashboard title and footer', () => {
// ===================== Server / HTTP =====================
test('server returns HTML on GET /', (done) => {
const { server } = require(SCRIPT);
// Server may or may not be listening — we start it on a random port
const testServer = http.createServer(server._events.request);
testServer.listen(0, () => {
const port = testServer.address().port;
http.get(`http://localhost:${port}/`, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8');
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
assert.ok(body.includes('<!DOCTYPE html>'));
assert.ok(body.includes('ECC Capabilities'));
testServer.close();
done();
});
});
test('resolveDashboardHost defaults to IPv4 loopback', () => {
const { resolveDashboardHost } = require(SCRIPT);
assert.strictEqual(resolveDashboardHost({}), '127.0.0.1');
assert.strictEqual(resolveDashboardHost({ ECC_DASHBOARD_HOST: '' }), '127.0.0.1');
});
test('resolveDashboardHost accepts only normalized loopback hosts', () => {
const { resolveDashboardHost } = require(SCRIPT);
assert.strictEqual(
resolveDashboardHost({ ECC_DASHBOARD_HOST: ' LOCALHOST ' }),
'localhost'
);
assert.strictEqual(
resolveDashboardHost({ ECC_DASHBOARD_HOST: '::1' }),
'::1'
);
assert.strictEqual(
resolveDashboardHost({ ECC_DASHBOARD_HOST: '[::1]' }),
'::1'
);
});
test('resolveDashboardHost rejects wildcard, LAN, and arbitrary hosts', () => {
const { resolveDashboardHost } = require(SCRIPT);
for (const host of ['0.0.0.0', '::', '192.168.1.10', 'dashboard.internal', '127.0.0.1:3456']) {
assert.throws(
() => resolveDashboardHost({ ECC_DASHBOARD_HOST: host }),
/ECC_DASHBOARD_HOST must be loopback-only/
);
}
});
test('listenDashboardServer always passes an explicit loopback host to listen', () => {
const { listenDashboardServer } = require(SCRIPT);
const calls = [];
const fakeServer = {
listen(...args) {
calls.push(args);
return this;
},
};
const onListening = () => {};
assert.strictEqual(
listenDashboardServer(fakeServer, {
host: '127.0.0.1',
onListening,
port: 3456,
}),
fakeServer
);
assert.deepStrictEqual(calls, [[3456, '127.0.0.1', onListening]]);
assert.throws(
() => listenDashboardServer(fakeServer, { host: '0.0.0.0', port: 3456 }),
/ECC_DASHBOARD_HOST must be loopback-only/
);
assert.strictEqual(calls.length, 1);
});
asyncTest('server returns no-store HTML on GET /', async () => {
await withDashboardServer(async (port) => {
const response = await requestDashboard(port);
assert.strictEqual(response.statusCode, 200);
assert.strictEqual(response.headers['content-type'], 'text/html; charset=utf-8');
assert.strictEqual(response.headers['cache-control'], 'no-store');
assert.ok(response.body.includes('<!DOCTYPE html>'));
assert.ok(response.body.includes('ECC Capabilities'));
});
});
test('server returns JSON on GET /api/data', (done) => {
const { server } = require(SCRIPT);
const testServer = http.createServer(server._events.request);
testServer.listen(0, () => {
const port = testServer.address().port;
http.get(`http://localhost:${port}/api/data`, (res) => {
assert.strictEqual(res.statusCode, 200);
assert.strictEqual(res.headers['content-type'], 'application/json');
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
const parsed = JSON.parse(body);
assert.ok(Array.isArray(parsed.agents));
assert.ok(Array.isArray(parsed.skills));
assert.ok(Array.isArray(parsed.commands));
assert.ok(Array.isArray(parsed.rules));
assert.ok(Array.isArray(parsed.mcps));
assert.ok(Array.isArray(parsed.hooks));
testServer.close();
done();
});
asyncTest('server returns no-store JSON on GET /api/data', async () => {
await withDashboardServer(async (port) => {
const response = await requestDashboard(port, { path: '/api/data' });
assert.strictEqual(response.statusCode, 200);
assert.strictEqual(response.headers['content-type'], 'application/json');
assert.strictEqual(response.headers['cache-control'], 'no-store');
const parsed = JSON.parse(response.body);
assert.ok(Array.isArray(parsed.agents));
assert.ok(Array.isArray(parsed.skills));
assert.ok(Array.isArray(parsed.commands));
assert.ok(Array.isArray(parsed.rules));
assert.ok(Array.isArray(parsed.mcps));
assert.ok(Array.isArray(parsed.hooks));
});
});
asyncTest('server returns a generic no-store 500 for data failures and remains usable', async () => {
let loadCount = 0;
const loggedErrors = [];
const emptyData = {
agents: [],
skills: [],
commands: [],
rules: [],
mcps: [],
hooks: [],
};
await withDashboardServer(async (port) => {
const failedResponse = await requestDashboard(port, { path: '/api/data' });
assert.strictEqual(failedResponse.statusCode, 500);
assert.strictEqual(failedResponse.headers['cache-control'], 'no-store');
assert.deepStrictEqual(JSON.parse(failedResponse.body), {
error: 'Internal server error',
});
assert.ok(!failedResponse.body.includes('sensitive loader detail'));
const followUpResponse = await requestDashboard(port, { path: '/api/data' });
assert.strictEqual(followUpResponse.statusCode, 200);
assert.deepStrictEqual(JSON.parse(followUpResponse.body), emptyData);
assert.strictEqual(loggedErrors.length, 1);
assert.strictEqual(loggedErrors[0].error.message, 'sensitive loader detail');
}, {
loadData: () => {
loadCount++;
if (loadCount === 1) {
throw new Error('sensitive loader detail');
}
return emptyData;
},
reportError: (message, error) => {
loggedErrors.push({ error, message });
},
});
});
asyncTest('server returns a generic no-store 500 for render failures and remains usable', async () => {
const { renderHTML } = require(SCRIPT);
let renderCount = 0;
const loggedErrors = [];
const emptyData = {
agents: [],
skills: [],
commands: [],
rules: [],
mcps: [],
hooks: [],
};
await withDashboardServer(async (port) => {
const failedResponse = await requestDashboard(port);
assert.strictEqual(failedResponse.statusCode, 500);
assert.strictEqual(failedResponse.headers['cache-control'], 'no-store');
assert.strictEqual(
failedResponse.body,
'<!DOCTYPE html><p>Dashboard unavailable.</p>'
);
assert.ok(!failedResponse.body.includes('sensitive render detail'));
const followUpResponse = await requestDashboard(port);
assert.strictEqual(followUpResponse.statusCode, 200);
assert.ok(followUpResponse.body.includes('ECC Capabilities'));
assert.strictEqual(loggedErrors.length, 1);
assert.strictEqual(loggedErrors[0].error.message, 'sensitive render detail');
}, {
loadData: () => emptyData,
render: (data) => {
renderCount++;
if (renderCount === 1) {
throw new Error('sensitive render detail');
}
return renderHTML(data);
},
reportError: (message, error) => {
loggedErrors.push({ error, message });
},
});
});
asyncTest('server rejects a missing or DNS-rebinding Host before routing', async () => {
await withDashboardServer(async (port) => {
const missingHost = await requestDashboardWithoutHost(port);
assert.strictEqual(missingHost.statusCode, 421);
assert.strictEqual(missingHost.headers['cache-control'], 'no-store');
const reboundHost = await requestDashboard(port, {
headers: { Host: 'dashboard.attacker.example' },
path: '/api/data',
});
assert.strictEqual(reboundHost.statusCode, 421);
assert.strictEqual(reboundHost.headers['cache-control'], 'no-store');
});
});
asyncTest('server rejects an allowed hostname with an invalid port without crashing', async () => {
await withDashboardServer(async (port) => {
const response = await requestDashboard(port, {
headers: { Host: 'localhost:99999' },
path: '/api/data',
});
assert.strictEqual(response.statusCode, 421);
assert.strictEqual(response.headers['cache-control'], 'no-store');
});
});
asyncTest('server returns a generic no-store 400 for a malformed absolute request target and remains usable', async () => {
await withDashboardServer(async (port) => {
const malformedResponse = await requestDashboard(port, {
path: 'http://attacker.example:99999/',
});
assert.strictEqual(malformedResponse.statusCode, 400);
assert.strictEqual(malformedResponse.headers['cache-control'], 'no-store');
assert.deepStrictEqual(JSON.parse(malformedResponse.body), {
error: 'Bad request',
});
const followUpResponse = await requestDashboard(port, {
path: '/api/data',
});
assert.strictEqual(followUpResponse.statusCode, 200);
assert.strictEqual(followUpResponse.headers['cache-control'], 'no-store');
});
});
asyncTest('server rejects cross-origin requests before routing', async () => {
await withDashboardServer(async (port) => {
const response = await requestDashboard(port, {
headers: { Origin: 'https://attacker.example' },
path: '/api/data',
});
assert.strictEqual(response.statusCode, 403);
assert.strictEqual(response.headers['cache-control'], 'no-store');
});
});
@ -853,5 +1154,21 @@ test('loadMcps handles empty mcp-configs directory', () => {
// ===================== Results =====================
console.log(`\nResults: Passed: ${testPassed}, Failed: ${testFailed}`);
process.exit(testFailed > 0 ? 1 : 0);
async function runAsyncTests() {
for (const { name, fn } of asyncTests) {
try {
await fn();
console.log(`${name}`);
testPassed++;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
testFailed++;
}
}
console.log(`\nResults: Passed: ${testPassed}, Failed: ${testFailed}`);
process.exitCode = testFailed > 0 ? 1 : 0;
}
runAsyncTests();