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

  1. Point the operation at a real path physically located inside an allowed root, not through an escaping symlink.
  2. Add the symlink's real target directory to the source's allowed_local_roots configuration if that target is legitimately permitted.
  3. On macOS/containers, configure roots using the resolved real path (e.g. /private/tmp instead of /tmp).
  4. 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

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


AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05). Data as JSON: /api/errors/71c06490689fb89e. Report an issue: GitHub.