feat(voyage): implement path-traversal + symlink/dotfile filter on loaded files
This commit is contained in:
parent
6293775f30
commit
cd6bca978f
3 changed files with 119 additions and 6 deletions
|
|
@ -98,5 +98,66 @@ test('voyage-playground.html stripUnsafeComments wired into renderArtifact (v4.3
|
|||
assert.match(text, /var\s+safeText\s*=\s*stripUnsafeComments\(/, 'renderArtifact must call stripUnsafeComments before md.render');
|
||||
});
|
||||
|
||||
// --- Step 26 placeholder — full filter test added by Sesjon 5 Step 26 ----
|
||||
// (Test below activates after Step 26 lands; kept as documentation stub.)
|
||||
// --- Step 26 — path-traversal + symlink/dotfile filter ------------------
|
||||
// Mirror of the browser-side isProjectPathSafe predicate. Kept verbatim so
|
||||
// the playground's filter cannot drift without breaking this test.
|
||||
function isProjectPathSafe(inside) {
|
||||
if (typeof inside !== 'string' || !inside) return false;
|
||||
if (inside.indexOf('..') !== -1) return false;
|
||||
if (inside.charAt(0) === '.') return false;
|
||||
if (inside.indexOf('/.') !== -1) return false;
|
||||
if (inside.indexOf('node_modules/') === 0 || inside.indexOf('/node_modules/') !== -1) return false;
|
||||
if (inside.indexOf('dist/') === 0 || inside.indexOf('/dist/') !== -1) return false;
|
||||
if (inside.indexOf('build/') === 0 || inside.indexOf('/build/') !== -1) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
test('isProjectPathSafe — rejects path-traversal (v4.3 Step 26)', () => {
|
||||
assert.equal(isProjectPathSafe('../etc/passwd'), false);
|
||||
assert.equal(isProjectPathSafe('foo/../etc/passwd'), false);
|
||||
assert.equal(isProjectPathSafe('a/b/../c'), false);
|
||||
});
|
||||
|
||||
test('isProjectPathSafe — rejects dotfiles at root + nested (v4.3 Step 26)', () => {
|
||||
assert.equal(isProjectPathSafe('.gitignore'), false);
|
||||
assert.equal(isProjectPathSafe('.git/config'), false);
|
||||
assert.equal(isProjectPathSafe('.DS_Store'), false);
|
||||
assert.equal(isProjectPathSafe('.env'), false);
|
||||
assert.equal(isProjectPathSafe('docs/.hidden/file'), false);
|
||||
assert.equal(isProjectPathSafe('research/.git/HEAD'), false);
|
||||
});
|
||||
|
||||
test('isProjectPathSafe — rejects node_modules / dist / build at any depth (v4.3 Step 26)', () => {
|
||||
assert.equal(isProjectPathSafe('node_modules/foo/index.js'), false);
|
||||
assert.equal(isProjectPathSafe('packages/sub/node_modules/x'), false);
|
||||
assert.equal(isProjectPathSafe('dist/bundle.js'), false);
|
||||
assert.equal(isProjectPathSafe('packages/x/dist/y.js'), false);
|
||||
assert.equal(isProjectPathSafe('build/output.js'), false);
|
||||
assert.equal(isProjectPathSafe('packages/x/build/y.js'), false);
|
||||
});
|
||||
|
||||
test('isProjectPathSafe — accepts valid project artifacts (v4.3 Step 26)', () => {
|
||||
assert.equal(isProjectPathSafe('brief.md'), true);
|
||||
assert.equal(isProjectPathSafe('plan.md'), true);
|
||||
assert.equal(isProjectPathSafe('review.md'), true);
|
||||
assert.equal(isProjectPathSafe('progress.json'), true);
|
||||
assert.equal(isProjectPathSafe('research/01-foo.md'), true);
|
||||
assert.equal(isProjectPathSafe('architecture/overview.md'), true);
|
||||
assert.equal(isProjectPathSafe('architecture/gaps.md'), true);
|
||||
});
|
||||
|
||||
test('isProjectPathSafe — fixture FileList survives filter to brief.md only (v4.3 Step 26)', () => {
|
||||
// Fixture mirroring Step 26 plan-Verify scenario: load a directory
|
||||
// containing the four hostile entries plus a valid brief.md and verify
|
||||
// only brief.md survives.
|
||||
const fixture = [
|
||||
'../etc/passwd',
|
||||
'.git/config',
|
||||
'node_modules/foo/index.js',
|
||||
'brief.md',
|
||||
'.DS_Store',
|
||||
'dist/junk.js',
|
||||
];
|
||||
const survivors = fixture.filter(isProjectPathSafe);
|
||||
assert.deepEqual(survivors, ['brief.md'], 'only brief.md should survive the filter');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -482,3 +482,23 @@ test('voyage-playground.html renderArtifact strips comments before md.render (v4
|
|||
const renderIdx = body.indexOf('md.render');
|
||||
assert.ok(stripIdx > 0 && stripIdx < renderIdx, 'stripUnsafeComments must run before md.render');
|
||||
});
|
||||
|
||||
// v4.3 Step 26 — path-traversal + symlink/dotfile filter.
|
||||
test('voyage-playground.html declares isProjectPathSafe filter (v4.3 Step 26)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /function\s+isProjectPathSafe\s*\(/, 'isProjectPathSafe() function required');
|
||||
// Must reject the four documented threat-classes
|
||||
assert.match(text, /indexOf\('\.\.'\)/, '..-rejection required');
|
||||
assert.match(text, /indexOf\('node_modules\//, 'node_modules/-rejection required');
|
||||
assert.match(text, /indexOf\('dist\//, 'dist/-rejection required');
|
||||
assert.match(text, /indexOf\('build\//, 'build/-rejection required');
|
||||
});
|
||||
|
||||
test('voyage-playground.html loadProjectDirectory wires isProjectPathSafe filter (v4.3 Step 26)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
// Must call the filter before classification, AND track filteredCount
|
||||
assert.match(text, /isProjectPathSafe\(inside\)/, 'isProjectPathSafe must be called on `inside` path');
|
||||
assert.match(text, /filteredCount\+\+/, 'filteredCount tracking required');
|
||||
// aria-live announce must fire when something is filtered
|
||||
assert.match(text, /announce\(filteredCount/, 'filteredCount announce required');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue