{"record":{"id":"205c97fa98b50a71","repo":"larksuite/cli","slug":"s-resolved-path-q-is-still-a-symlink","errorCode":null,"errorMessage":"%s: resolved path %q is still a symlink","messagePattern":"(.+?): resolved path %q is still a symlink","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/binding/audit.go","lineNumber":113,"sourceCode":"// is not a symlink, target is returned unchanged. A symlink that points to\n// another symlink is rejected so callers only deal with a single hop.\nfunc resolveSymlinkIfAllowed(target string, linfo fs.FileInfo, params AuditParams) (string, error) {\n\tif linfo.Mode()&os.ModeSymlink == 0 {\n\t\treturn target, nil\n\t}\n\tif !params.AllowSymlinkPath {\n\t\treturn \"\", fmt.Errorf(\"%s: path %q is a symlink (not allowed)\", params.Label, target)\n\t}\n\tresolved, err := vfs.EvalSymlinks(target)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%s: cannot resolve symlink %q: %w\", params.Label, target, err)\n\t}\n\trinfo, err := vfs.Lstat(resolved)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%s: cannot stat resolved path %q: %w\", params.Label, resolved, err)\n\t}\n\tif rinfo.Mode()&os.ModeSymlink != 0 {\n\t\treturn \"\", fmt.Errorf(\"%s: resolved path %q is still a symlink\", params.Label, resolved)\n\t}\n\treturn resolved, nil\n}\n\n// requireInTrustedDirs enforces that effectivePath lives under one of the\n// caller-declared trusted directories, if any were declared. An empty\n// trustedDirs list disables the check.\nfunc requireInTrustedDirs(effectivePath string, trustedDirs []string, label string) error {\n\tif len(trustedDirs) == 0 {\n\t\treturn nil\n\t}\n\tcleaned := filepath.Clean(effectivePath)\n\tfor _, dir := range trustedDirs {\n\t\tcleanDir := filepath.Clean(dir)\n\t\tif cleaned == cleanDir || strings.HasPrefix(cleaned, cleanDir+\"/\") {\n\t\t\treturn nil\n\t\t}\n\t}","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/binding/audit.go#L95-L131","documentation":"This library only permits a single hop of symlink indirection. After EvalSymlinks resolves the target, resolveSymlinkIfAllowed Lstats the result and rejects it if the resolved path is still a symlink (i.e. a symlink-to-symlink chain). This keeps the audit surface predictable: callers always receive a final, non-link effective path.","triggerScenarios":"The configured path is a symlink whose target is itself another symlink (e.g. /usr/local/bin/tool -> /opt/tool/current -> /opt/tool/2.1.0/bin/tool) and AllowSymlinkPath is true.","commonSituations":"Version-manager layouts (nvm, pyenv, update-alternatives, /etc/alternatives chains) where 'current' is itself a link; manually chained links created during a tool upgrade; distro wrappers pointing at alternatives directories.","solutions":["Resolve the full chain yourself and configure the final concrete path (readlink -f <path>)","Rebuild the first symlink to point directly at the final file instead of an intermediate link","If intermediate links are required by your setup, generate config with the fully-resolved path at install time"],"exampleFix":"// before\ncfg.path = \"/usr/local/bin/tool\" // -> /opt/tool/current -> /opt/tool/2.1.0/tool\n// after\ncfg.path = readlink -f /usr/local/bin/tool // /opt/tool/2.1.0/tool","handlingStrategy":"validation","validationCode":"func ensureSingleHop(p string) (string, error) {\n  fi, err := os.Lstat(p)\n  if err != nil { return \"\", err }\n  if fi.Mode()&os.ModeSymlink == 0 { return p, nil }\n  resolved, err := filepath.EvalSymlinks(p)\n  if err != nil { return \"\", err }\n  rfi, err := os.Lstat(resolved)\n  if err != nil { return \"\", err }\n  if rfi.Mode()&os.ModeSymlink != 0 { return \"\", fmt.Errorf(\"%s resolves to another symlink\", p) }\n  return resolved, nil\n}","typeGuard":"func isStillSymlink(fi fs.FileInfo) bool { return fi.Mode()&os.ModeSymlink != 0 }","tryCatchPattern":null,"preventionTips":["Store fully-resolved paths (readlink -f) in config instead of 'current' style links","Re-generate config when upgrading tools that use intermediate symlink indirection","Prefer direct paths over alternatives/update-alternatives entries"],"tags":["filesystem","symlink","security-audit"],"backgroundTag":"symlink-chain-rejected","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}