larksuite/cli · error

%s: path %q is a symlink (not allowed)

Error message

%s: path %q is a symlink (not allowed)

What it means

resolveSymlinkIfAllowed rejects the audited path because it is a symlink and AuditParams.AllowSymlinkPath is false. The secure-path audit refuses symlink indirection by default to prevent path-traversal/link-swap attacks; even when allowed, only a single hop is resolved and a symlink chain is rejected.

Source

Thrown at internal/binding/audit.go:102

	if err != nil {
		return nil, fmt.Errorf("%s: cannot stat %q: %w", label, target, err)
	}
	if info.IsDir() {
		return nil, fmt.Errorf("%s: path %q is a directory, not a file", label, target)
	}
	return info, nil
}

// resolveSymlinkIfAllowed resolves a symlink to its target when
// params.AllowSymlinkPath is true, or rejects it otherwise. When the input
// is not a symlink, target is returned unchanged. A symlink that points to
// another symlink is rejected so callers only deal with a single hop.
func resolveSymlinkIfAllowed(target string, linfo fs.FileInfo, params AuditParams) (string, error) {
	if linfo.Mode()&os.ModeSymlink == 0 {
		return target, nil
	}
	if !params.AllowSymlinkPath {
		return "", fmt.Errorf("%s: path %q is a symlink (not allowed)", params.Label, target)
	}
	resolved, err := vfs.EvalSymlinks(target)
	if err != nil {
		return "", fmt.Errorf("%s: cannot resolve symlink %q: %w", params.Label, target, err)
	}
	rinfo, err := vfs.Lstat(resolved)
	if err != nil {
		return "", fmt.Errorf("%s: cannot stat resolved path %q: %w", params.Label, resolved, err)
	}
	if rinfo.Mode()&os.ModeSymlink != 0 {
		return "", fmt.Errorf("%s: resolved path %q is still a symlink", params.Label, resolved)
	}
	return resolved, nil
}

// requireInTrustedDirs enforces that effectivePath lives under one of the
// caller-declared trusted directories, if any were declared. An empty
// trustedDirs list disables the check.

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Set AllowSymlinkPath: true in AuditParams if symlinked configs are acceptable in your environment.
  2. Replace the symlink with a real file copy (cp --remove-destination or rm the link and cp the target).
  3. Point the binding directly at the symlink's real target path so no link is audited.
  4. If AllowSymlinkPath is true but it still fails, check the resolved target: a second-hop symlink (link to a link) is rejected — flatten the chain to one hop.

Example fix

// before
params := binding.AuditParams{Label: "config"}
// after
params := binding.AuditParams{Label: "config", AllowSymlinkPath: true}
Defensive patterns

Strategy: validation

Validate before calling

// preflight: detect symlink and flatten to real path if policy disallows links
if info, err := os.Lstat(p); err == nil && info.Mode()&os.ModeSymlink != 0 && !params.AllowSymlinkPath {
    real, err := filepath.EvalSymlinks(p)
    if err != nil { return err }
    p = real // or copy the file over the link
}

Type guard

func isSymlinkErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "is a symlink (not allowed)")
}

Try / catch

if err := binding.AssertSecurePath(p, params); err != nil {
    if isSymlinkErr(err) {
        if real, e := filepath.EvalSymlinks(p); e == nil {
            return binding.AssertSecurePath(real, params)
        }
    }
    return err
}

Prevention

When it happens

Trigger: AssertSecurePath -> resolveSymlinkIfAllowed where linfo.Mode() has os.ModeSymlink and params.AllowSymlinkPath is false — the file being audited is a symlink (e.g. dotfile managers, synced folders, or package-managed symlinks).

Common situations: Dotfile managers (stow, chezmoi, ln -s) symlink config files into a repo; cloud-sync folders replacing files with symlinks; CI checkout creating symlinks; user manually linking a shared config across projects.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/971ee6ac28975ce6. Report an issue: GitHub.