fix: allow paths below a filesystem-root workspace root (#3277)

Fixes #2808

`validatePath()` builds the allowed prefix as `canonicalRoot +
path.sep`. A filesystem root (`/`, or `C:\` on Windows) already ends
with the separator, so the prefix becomes `//` (or `C:\\`) and no path
below it matches. This makes `--workspace=/`, which the
`--allowUnrestrictedPaths` deprecation notice recommends, reject every
file write with `Access denied`.

The fix appends the separator only when the root doesn't already end
with one. Narrower roots behave as before.

The new test in `tests/roots.test.ts` sets the workspace root to the
filesystem root of the path under test, so it is on the same drive on
Windows (that drive mismatch is why the CI failed on the earlier, stale
attempt in #2809). It fails on `main` with `Access denied` and passes
with the fix. `tests/roots.test.ts` passes locally.

---
AI-assistance note: I prepared this change with help from an AI coding
assistant, then reviewed it and ran the tests myself.
This commit is contained in:
kevin authored and GitHub committed 2026-10-22 12:25:50 +00:00
1 parent 3eb76a6547
commit 296ccef3dd
2 files changed
+23 -1

No files matched your search

+6 -1
View File
@@ -537,9 +537,14 @@ export class McpContext implements Context, BrowserlessContext {
if (result.status === 'fulfilled') {
const canonicalRoot = result.value;
// A filesystem root such as `/` or `C:\` already ends with the
// separator; appending another one would match no path below it.
const rootPrefix = canonicalRoot.endsWith(path.sep)
? canonicalRoot
: canonicalRoot + path.sep;
if (
canonicalPath === canonicalRoot ||
canonicalPath.startsWith(canonicalRoot + path.sep)
canonicalPath.startsWith(rootPrefix)
) {
allowed = true;
break;
+17
View File
@@ -181,6 +181,23 @@ describe('McpContext path validation escaping', () => {
});
}
it('allows paths below a workspace root that is the filesystem root', async () => {
const context = await createContext();
const targetPath = path.resolve(
os.homedir(),
'a_very_unlikely_path_name_filesystem_root',
);
const canonicalPath = await resolveCanonicalPath(targetPath);
// Use the root of the canonical path so the test also holds when the home
// directory goes through a symlink or junction onto another drive.
const filesystemRoot = path.parse(canonicalPath).root;
context.setRoots([
{uri: pathToFileURL(filesystemRoot).href, name: 'filesystem root'},
]);
const resolved = await context.validatePath(targetPath);
assert.strictEqual(resolved, canonicalPath);
});
it('escapes the file path when it cannot be resolved', async () => {
const context = await createContext();
const filePath = path.join(os.tmpdir(), 'file.txt', INJECTED);