modelcontextprotocol/servers · error · Error
Access denied - symlink target outside allowed directories:
Error message
Access denied - symlink target outside allowed directories: ${currentPath} not in ${allowedDirectories.join(', ')} What it means
While resolving each path component, the library calls fs.realpath on the matched entry and re-checks that the resolved target stays inside the allowed directories. If a symlink points outside the sandbox, resolution is aborted with this error to block symlink-escape attacks. This is an intentional security guard, not a bug.
Source
Thrown at src/filesystem/lib.ts:133
const exactMatch = entries.find(entry => entry === requestedPart);
const equivalentMatches = exactMatch
? [exactMatch]
: entries.filter(entry => entry.normalize('NFC') === requestedPart.normalize('NFC'));
if (equivalentMatches.length > 1) {
throw new Error(`Ambiguous Unicode path component: ${requestedPart}`);
}
if (equivalentMatches.length === 0) {
// Nothing below this point exists yet, so there are no symlinks left to
// resolve. currentPath is already realpath'd and inside an allowed
// directory; append the missing tail so create_directory can mkdir -p it.
return path.join(currentPath, ...relativeParts.slice(index));
}
currentPath = await fs.realpath(path.join(currentPath, equivalentMatches[0]));
if (!isPathWithinAllowedDirectories(normalizePath(currentPath), allowedDirectories)) {
throw new Error(`Access denied - symlink target outside allowed directories: ${currentPath} not in ${allowedDirectories.join(', ')}`);
}
}
return currentPath;
}
export async function validatePath(requestedPath: string): Promise<string> {
const expandedPath = expandHome(requestedPath);
// Do not silently reinterpret a Windows drive path as a relative POSIX path.
// This would create a literal filename such as `C:\\Users\\...` inside the
// allowed root and report success for the wrong location.
if (process.platform !== 'win32' && /^(?:[A-Za-z]:)(?:[\\/]|$)/.test(expandedPath)) {
throw new Error(`Access denied - Windows-style path received on a POSIX host: ${requestedPath}`);
}
const absolute = path.isAbsolute(expandedPath)
? path.resolve(expandedPath)
: resolveRelativePathAgainstAllowedDirectories(expandedPath);
View on GitHub (pinned to 579c3903f3)
Solutions
- Remove or retarget the symlink so its realpath stays inside the allowed directories
- Add the symlink's real target directory to the server's allowedDirectories argument (if policy permits) and restart
- Replace the symlink with a bind mount or copy of the content inside the allowed root
Example fix
# before
ln -s /etc/passwd /allowed/data/passwd-link
read_file('/allowed/data/passwd-link') # Access denied
# after: keep target inside sandbox
cp /etc/passwd /allowed/data/passwd.txt
read_file('/allowed/data/passwd.txt') Defensive patterns
Strategy: validation
Validate before calling
const real = await fs.realpath(p).catch(() => null);
const allowed = [ /* configured allowedDirectories */ ];
if (real && !allowed.some(dir => real === dir || real.startsWith(dir + path.sep))) {
throw new Error(`symlink target outside sandbox: ${real}`);
} Type guard
function isWithinAllowed(realPath: string, allowedDirs: string[]): boolean {
return allowedDirs.some(dir => realPath === dir || realPath.startsWith(dir + path.sep));
} Prevention
- Audit symlinks under allowed directories periodically (find -type l -exec realpath {})
- Avoid creating convenience symlinks to system or external paths inside sandboxed roots
- Pass a minimal, explicit allowedDirectories list to the server
- Prefer bind mounts or copies over symlinks when sharing content into the sandbox
When it happens
Trigger: Accessing any path (via validatePath) where a component in the chain is a symlink whose realpath resolves outside the directories configured at server startup — even if the requested path textually lies inside an allowed root.
Common situations: Users pointing tools at convenience links like ~/shared -> /etc or /home/user/data -> /mnt/external that were created before the server was restricted to allowedDirectories; Docker/CI mounts where allowed dirs differ from the link target.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Access denied - parent directory outside allowed directories
- Access denied - symlink target outside allowed directories:
- Invalid start_timestamp: '{start_timestamp}' - cannot start
- Invalid end_timestamp: '{end_timestamp}' - cannot start with
- Parent directory does not exist: ${parentDir}
AI-assisted analysis of modelcontextprotocol/servers@579c3903f3 (2026-09-01).
Data as JSON: /api/errors/f64bd0ccfd9964e0.
Report an issue: GitHub.