mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 11:22:34 +00:00
62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
/**
|
|
* #774 NAV-1/NAV-2 path-containment guard (`isWithinRoot` in sync.ts).
|
|
*
|
|
* The guard decides whether a realpath-resolved file lives inside the
|
|
* realpath-resolved git root. It backs BOTH the `--src-subpath` scope-entry
|
|
* check and the per-file TOCTOU re-check in the incremental import drain.
|
|
*
|
|
* It used to be a string prefix test (`real.startsWith(rootReal + '/')`). That
|
|
* hardcoded separator makes the guard fail CLOSED for every path on Windows,
|
|
* where `realpathSync` returns `C:\repo\file.md` and the `C:\repo/` prefix can
|
|
* never match: every file in the drain is recorded as a symlink escape and the
|
|
* sync imports nothing. These cases pin the containment semantics against the
|
|
* platform's own separator, so a future prefix-based rewrite is caught.
|
|
*
|
|
* The Windows regression itself is only OBSERVABLE on win32 — on POSIX the old
|
|
* prefix form and the current `relative()` form agree on every case here.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { join, sep, parse } from 'path';
|
|
import { isWithinRoot } from '../src/commands/sync.ts';
|
|
|
|
describe('isWithinRoot path containment (#774 NAV-1/NAV-2)', () => {
|
|
const root = join(parse(process.cwd()).root, 'repo');
|
|
|
|
test('accepts the root itself', () => {
|
|
expect(isWithinRoot(root, root)).toBe(true);
|
|
});
|
|
|
|
test('accepts a direct child', () => {
|
|
expect(isWithinRoot(join(root, 'page.md'), root)).toBe(true);
|
|
});
|
|
|
|
test('accepts a nested descendant (the case a hardcoded "/" broke on win32)', () => {
|
|
expect(isWithinRoot(join(root, 'wiki', 'notes', 'page.md'), root)).toBe(true);
|
|
});
|
|
|
|
test('rejects the parent directory', () => {
|
|
expect(isWithinRoot(parse(root).dir, root)).toBe(false);
|
|
});
|
|
|
|
test('rejects a path escaping upward', () => {
|
|
expect(isWithinRoot(join(root, '..', 'outside', 'page.md'), root)).toBe(false);
|
|
});
|
|
|
|
test('rejects a sibling sharing the root as a string prefix', () => {
|
|
expect(isWithinRoot(root + '-evil', root)).toBe(false);
|
|
expect(isWithinRoot(join(root + '-evil', 'page.md'), root)).toBe(false);
|
|
});
|
|
|
|
test('rejects a sibling whose name extends the last segment', () => {
|
|
expect(isWithinRoot(join(parse(root).dir, 'repository', 'page.md'), root)).toBe(false);
|
|
});
|
|
|
|
test('containment does not depend on a hardcoded forward slash', () => {
|
|
// On win32 `sep` is '\\'; the child below is genuinely inside `root` and a
|
|
// `root + '/'` prefix test would have rejected it.
|
|
const child = `${root}${sep}sub${sep}page.md`;
|
|
expect(isWithinRoot(child, root)).toBe(true);
|
|
});
|
|
});
|