{"record":{"id":"71c06490689fb89e","repo":"googleapis/mcp-toolbox","slug":"local-path-q-resolves-through-a-symbolic-link-to","errorCode":null,"errorMessage":"local path %q resolves through a symbolic link to a target outside the allowed local roots for source %q","messagePattern":"local path %q resolves through a symbolic link to a target outside the allowed local roots for source %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"internal/sources/cloudstorage/cloudstorage.go","lineNumber":151,"sourceCode":"\t\treturn fmt.Errorf(\"local path %q is not under any allowed local roots for source %q\", p, s.Name)\n\t}\n\n\tresolved, err := cloudstoragecommon.ResolveSymlinks(clean)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"local path %q cannot be resolved for source %q: %w\", p, s.Name, err)\n\t}\n\tfor _, root := range s.AllowedLocalRoots {\n\t\t// A root we cannot resolve authorizes nothing; skip it rather than\n\t\t// falling back to the name-level match we already passed.\n\t\tresolvedRoot, err := cloudstoragecommon.ResolveSymlinks(root)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif isUnderRoot(resolved, resolvedRoot) {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"local path %q resolves through a symbolic link to a target outside the allowed local roots for source %q\", p, s.Name)\n}\n\nfunc isUnderRoot(target, root string) bool {\n\ttarget = filepath.Clean(target)\n\troot = filepath.Clean(root)\n\tif target == root {\n\t\treturn true\n\t}\n\tif root == string(filepath.Separator) {\n\t\treturn true\n\t}\n\tsep := string(filepath.Separator)\n\tif !strings.HasSuffix(root, sep) {\n\t\troot += sep\n\t}\n\treturn strings.HasPrefix(target, root)\n}\n","sourceCodeStart":133,"sourceCodeEnd":169,"githubUrl":"https://github.com/googleapis/mcp-toolbox/blob/8cc6e09de2ad7b8bffc77751799585a1401a48eb/internal/sources/cloudstorage/cloudstorage.go#L133-L169","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nsource.DownloadObject(ctx, \"bkt\", \"obj\", \"/data/link/secret.txt\") // /data/link -> /etc\n// after\nsource.DownloadObject(ctx, \"bkt\", \"obj\", \"/data/real/secret.txt\") // physically under an allowed root","handlingStrategy":"validation","validationCode":"resolved, err := filepath.EvalSymlinks(localPath)\nif err != nil { return err }\nfor _, root := range allowedRoots {\n    rr, err := filepath.EvalSymlinks(root)\n    if err != nil { continue }\n    if strings.HasPrefix(resolved, rr+string(filepath.Separator)) || resolved == rr {\n        return nil\n    }\n}\nreturn errors.New(\"path escapes allowed roots\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["security","symlink","path-traversal","sandbox-escape","cloudstorage"],"backgroundTag":"symlink-escape-blocked","analyzedSha":"8cc6e09de2ad7b8bffc77751799585a1401a48eb","analyzedAt":"2026-09-05T01:10:36.887Z","contentChangedAt":"2026-09-05T01:10:36.887Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}