Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions modules/jarvos-gbrain/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,23 @@ function resolveSourcePath(item, config) {
const sourcePath = firstString(item.sourcePath, item.path);
if (!sourcePath) return null;
const expanded = expandTilde(sourcePath);
if (path.isAbsolute(expanded)) return expanded;
return path.join(config.vaultDir, expanded);
const vaultDir = path.resolve(config.vaultDir);
const resolved = path.resolve(vaultDir, expanded);
const relative = path.relative(vaultDir, resolved);
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
return resolved;
}

function resolveRealSourcePath(sourcePath, config) {
try {
const vaultDir = fs.realpathSync(config.vaultDir);
const resolved = fs.realpathSync(sourcePath);
const relative = path.relative(vaultDir, resolved);
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
return resolved;
} catch {
return null;
}
}

function targetPathForItem(item, config) {
Expand Down Expand Up @@ -354,20 +369,25 @@ function createImportPlan(overrides = {}) {

const sourcePath = resolveSourcePath(item, config);
if (!sourcePath) {
warnings.push(`Item ${index} is missing sourcePath`);
warnings.push(`Item ${index} has a missing or out-of-vault sourcePath`);
continue;
}
if (!fs.existsSync(sourcePath)) {
warnings.push(`Item ${index} source does not exist: ${sourcePath}`);
continue;
}
const realSourcePath = resolveRealSourcePath(sourcePath, config);
if (!realSourcePath) {
warnings.push(`Item ${index} source is outside the vault: ${sourcePath}`);
continue;
}

const targetPath = targetPathForItem(item, config);
items.push({
index,
type: pageType,
title: firstString(item.title, path.basename(sourcePath, '.md')),
sourcePath,
sourcePath: realSourcePath,
targetPath,
slug: path.basename(targetPath, '.md'),
tags: Array.isArray(item.tags) ? item.tags : [],
Expand Down
45 changes: 45 additions & 0 deletions modules/jarvos-gbrain/test/gbrain.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,51 @@ test('createImportPlan maps curated manifest items to GBrain targets', () => {
assert.equal(plan.warnings.length, 1);
});

test('createImportPlan rejects source paths outside the configured vault', () => {
const root = tempDir();
const vault = path.join(root, 'vault');
const brain = path.join(root, 'brain');
const outsideNote = path.join(root, 'secret.md');
const manifestPath = path.join(root, 'manifest.json');

fs.mkdirSync(vault, { recursive: true });
fs.writeFileSync(outsideNote, 'must not be imported', 'utf8');
fs.writeFileSync(manifestPath, JSON.stringify({
version: 1,
items: [
{ type: 'source', sourcePath: '../secret.md' },
{ type: 'source', sourcePath: outsideNote },
],
}), 'utf8');

const plan = gbrain.createImportPlan({ vaultDir: vault, brainDir: brain, manifestPath });

assert.equal(plan.itemCount, 0);
assert.equal(plan.warnings.length, 2);
assert.ok(plan.warnings.every((warning) => /out-of-vault/.test(warning)));
});

test('createImportPlan rejects symlinks that escape the configured vault', () => {
const root = tempDir();
const vault = path.join(root, 'vault');
const outsideNote = path.join(root, 'secret.md');
const link = path.join(vault, 'linked-secret.md');
const manifestPath = path.join(root, 'manifest.json');

fs.mkdirSync(vault, { recursive: true });
fs.writeFileSync(outsideNote, 'must not be imported', 'utf8');
fs.symlinkSync(outsideNote, link);
fs.writeFileSync(manifestPath, JSON.stringify({
version: 1,
items: [{ type: 'source', sourcePath: 'linked-secret.md' }],
}), 'utf8');

const plan = gbrain.createImportPlan({ vaultDir: vault, manifestPath });

assert.equal(plan.itemCount, 0);
assert.match(plan.warnings[0], /outside the vault/);
});

test('importToBrain dry-run does not write generated pages', () => {
const root = tempDir();
const vault = path.join(root, 'vault');
Expand Down
Loading