From a8be681f5282f3b479576a856ad386779f6a59d1 Mon Sep 17 00:00:00 2001 From: levineam Date: Fri, 14 Aug 2026 14:34:05 -0400 Subject: [PATCH] fix(agent-context): validate secondbrain module root --- modules/jarvos-agent-context/src/index.js | 26 +++++++++++++++++-- .../test/agent-context.test.js | 14 ++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/modules/jarvos-agent-context/src/index.js b/modules/jarvos-agent-context/src/index.js index c833a170..957f1061 100644 --- a/modules/jarvos-agent-context/src/index.js +++ b/modules/jarvos-agent-context/src/index.js @@ -150,8 +150,30 @@ function routeThreadKey(input = {}) { // Defaults to the bundled modules copy; set JARVOS_SECONDBRAIN_DIR to an absolute // path (e.g. the canonical clawd mirror) to point all note-creation through it. function secondbrainDir() { - return expandTilde(process.env.JARVOS_SECONDBRAIN_DIR) - || path.join(JARVOS_ROOT, 'modules', 'jarvos-secondbrain'); + const configured = expandTilde(process.env.JARVOS_SECONDBRAIN_DIR); + const candidate = configured || path.join(JARVOS_ROOT, 'modules', 'jarvos-secondbrain'); + if (!path.isAbsolute(candidate)) { + throw new Error('JARVOS_SECONDBRAIN_DIR must be an absolute path'); + } + + let trustedRoot; + try { + trustedRoot = fs.realpathSync(candidate); + } catch { + throw new Error('JARVOS_SECONDBRAIN_DIR must identify an existing directory'); + } + + const uid = typeof process.getuid === 'function' ? process.getuid() : null; + for (let current = trustedRoot; ; current = path.dirname(current)) { + const stat = fs.statSync(current); + const trustedOwner = uid === null || stat.uid === uid || stat.uid === 0; + if (!stat.isDirectory() || !trustedOwner || (stat.mode & 0o022) !== 0) { + throw new Error('JARVOS_SECONDBRAIN_DIR must be in an owner-controlled directory tree'); + } + const parent = path.dirname(current); + if (parent === current) break; + } + return trustedRoot; } function loadJarvosPaths() { diff --git a/modules/jarvos-agent-context/test/agent-context.test.js b/modules/jarvos-agent-context/test/agent-context.test.js index 209b070d..0347df3c 100644 --- a/modules/jarvos-agent-context/test/agent-context.test.js +++ b/modules/jarvos-agent-context/test/agent-context.test.js @@ -286,6 +286,20 @@ test('createNote writes note, links journal, and verifies contract', () => { }); }); +test('createNote rejects an untrusted secondbrain module root before loading code', () => { + const previous = process.env.JARVOS_SECONDBRAIN_DIR; + process.env.JARVOS_SECONDBRAIN_DIR = 'relative/attacker-controlled-secondbrain'; + try { + assert.throws( + () => createNote({ title: 'Unsafe module root', content: 'Must not execute.' }), + /JARVOS_SECONDBRAIN_DIR must be an absolute path/, + ); + } finally { + if (previous === undefined) delete process.env.JARVOS_SECONDBRAIN_DIR; + else process.env.JARVOS_SECONDBRAIN_DIR = previous; + } +}); + test('createNote creates today journal when missing', () => { withTempVault(({ journal, mutationService }) => { const result = createNote({