googleapis/mcp-toolbox · critical
local path %q resolves through a symbolic link to a target o
Error message
local path %q resolves through a symbolic link to a target outside the allowed local roots for source %q
What it means
The local path resolves (after symlink traversal) to a real location outside every allowed local root configured on the source. This is a deliberate security guard against symlink escape: the path matched by name, but its true target lies outside the whitelisted roots, so the operation is refused.
Source
Thrown at internal/sources/cloudstorage/cloudstorage.go:151
return fmt.Errorf("local path %q is not under any allowed local roots for source %q", p, s.Name)
}
resolved, err := cloudstoragecommon.ResolveSymlinks(clean)
if err != nil {
return fmt.Errorf("local path %q cannot be resolved for source %q: %w", p, s.Name, err)
}
for _, root := range s.AllowedLocalRoots {
// A root we cannot resolve authorizes nothing; skip it rather than
// falling back to the name-level match we already passed.
resolvedRoot, err := cloudstoragecommon.ResolveSymlinks(root)
if err != nil {
continue
}
if isUnderRoot(resolved, resolvedRoot) {
return nil
}
}
return fmt.Errorf("local path %q resolves through a symbolic link to a target outside the allowed local roots for source %q", p, s.Name)
}
func isUnderRoot(target, root string) bool {
target = filepath.Clean(target)
root = filepath.Clean(root)
if target == root {
return true
}
if root == string(filepath.Separator) {
return true
}
sep := string(filepath.Separator)
if !strings.HasSuffix(root, sep) {
root += sep
}
return strings.HasPrefix(target, root)
}
View on GitHub (pinned to 8cc6e09de2)
Solutions
- Point the operation at a real path physically located inside an allowed root, not through an escaping symlink.
- Add the symlink's real target directory to the source's allowed_local_roots configuration if that target is legitimately permitted.
- On macOS/containers, configure roots using the resolved real path (e.g. /private/tmp instead of /tmp).
- If the symlink is unexpected, remove it and audit how it was created (possible compromise).
Example fix
// before source.DownloadObject(ctx, "bkt", "obj", "/data/link/secret.txt") // /data/link -> /etc // after source.DownloadObject(ctx, "bkt", "obj", "/data/real/secret.txt") // physically under an allowed root
Defensive patterns
Strategy: validation
Validate before calling
resolved, err := filepath.EvalSymlinks(localPath)
if err != nil { return err }
for _, root := range allowedRoots {
rr, err := filepath.EvalSymlinks(root)
if err != nil { continue }
if strings.HasPrefix(resolved, rr+string(filepath.Separator)) || resolved == rr {
return nil
}
}
return errors.New("path escapes allowed roots") Prevention
- Configure allowed_local_roots with real, resolved directory paths, never through symlinks.
- Reject user-supplied paths containing symlink components; resolve before accepting.
- On macOS/containers remember /tmp-style aliases (/private/tmp) and use resolved paths.
- Monitor for this error in production logs - it may indicate a symlink-planting attack.
When it happens
Trigger: DownloadObject/UploadObject called with a path under an allowed root only via a symlink whose target points elsewhere (e.g. /allowed/data -> /etc, or /allowed/link -> /home/user/secrets). validateLocalPath resolves the final target and no resolved root contains it.
Common situations: Attacker-controlled or user-supplied path planting a symlink to escape a sandboxed directory; container images where /tmp is a symlink to /private/tmp (macOS) or a different mount; admins configuring allowed_local_roots with the symlink path instead of the real target directory.
Related errors
- path %q resolves through a symbolic link to a target outside
- local path %q cannot be resolved for source %q: %w
- path %q cannot be resolved: %w
- path cannot contain dot segments (..)
- failed to parse and verify JWT token: %w
AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05).
Data as JSON: /api/errors/71c06490689fb89e.
Report an issue: GitHub.