googleapis/mcp-toolbox · error

path %q resolves through a symbolic link to a target outside

Error message

path %q resolves through a symbolic link to a target outside configured directory %q

What it means

This is the deliberate security rejection of ResolveWithinDir: after symlink resolution, the destination path points outside the configured allowed directory. The tool refuses to operate to prevent symlink-based path traversal (writing or reading outside the sandboxed directory). It is not a bug — it is the path-traversal guard firing.

Source

Thrown at internal/tools/cloudstorage/cloudstoragecommon/paths.go:159

		return "", fmt.Errorf("path %q escapes configured directory %q", rel, cleanDir)
	}

	// Repeat the check against the real targets. A symlink under cleanDir can
	// point anywhere, so the name-level check above proves nothing on its own.
	resolvedDir, err := ResolveSymlinks(cleanDir)
	if err != nil {
		return "", fmt.Errorf("directory %q cannot be resolved: %w", cleanDir, err)
	}
	resolvedDest, err := ResolveSymlinks(cleanDest)
	if err != nil {
		return "", fmt.Errorf("path %q cannot be resolved: %w", rel, err)
	}
	out, err = escapes(resolvedDir, resolvedDest)
	if err != nil {
		return "", fmt.Errorf("path %q cannot be resolved within %q: %w", rel, cleanDir, err)
	}
	if out {
		return "", fmt.Errorf("path %q resolves through a symbolic link to a target outside configured directory %q", rel, cleanDir)
	}
	return cleanDest, nil
}

View on GitHub (pinned to 8cc6e09de2)

Solutions

  1. Choose a destination path that does not traverse symlinks outside the allowed directory
  2. Replace the symlink inside the allowed directory with a real file/directory
  3. If the escape is intentional, reconfigure the allowed directory in the source to include the real target
  4. Audit the path with `readlink -f <path>` to see where it actually resolves

Example fix

// before: ln -s /etc/passwd uploads/escape && copy to uploads/escape
// after
rm uploads/escape
touch uploads/realfile.txt  # use a real path inside the allowed directory
Defensive patterns

Strategy: validation

Validate before calling

func resolvesInside(base, p string) (bool, error) {
    r, err := filepath.EvalSymlinks(p); if err != nil { return false, err }
    rb, err := filepath.EvalSymlinks(base); if err != nil { return false, err }
    rel, err := filepath.Rel(rb, r); if err != nil { return false, err }
    return rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)), nil
}

Type guard

func isRealPathInside(base, p string) bool {
    escaped, err := resolvesInside(base, p)
    return err == nil && !escaped
}

Try / catch

out, err := ResolveWithinDir(baseDir, userPath)
if err != nil && strings.Contains(err.Error(), "resolves through a symbolic link") {
    http.Error(w, "destination escapes allowed directory", http.StatusBadRequest)
    return
}
if err != nil { return err }

Prevention

When it happens

Trigger: A user-supplied 'dest' path contains (or traverses through) a symlink whose target resolves outside the configured directory, so escapes() returns true and the tool rejects the operation.

Common situations: Attacker or careless user places a symlink inside the upload dir pointing to /etc or another bucket-visible path; Docker/K8s volume mounts that create symlinks outside the allowed root; users using '..' components combined with symlinks.

Related errors


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