{"record":{"id":"8fd00797c8845fc8","repo":"jdx/mise","slug":"refusing-to-follow-symlink-from-an-untrusted-pa","errorCode":null,"errorMessage":"refusing to follow symlink {} from an untrusted parent directory","messagePattern":"refusing to follow symlink (.+?) from an untrusted parent directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/managed_files.rs","lineNumber":1612,"sourceCode":"            _ => bail!(\"invalid managed directory path: {}\", path.display()),\n        })\n        .collect::<Result<Vec<_>>>()?;\n\n    let flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW;\n    let mut directory = open(Path::new(\"/\"), flags, Mode::empty())?;\n    let mut current = PathBuf::from(\"/\");\n    for (index, name) in components.iter().enumerate() {\n        let component_path = current.join(name);\n        directory = match openat(&directory, name.as_os_str(), flags, Mode::empty()) {\n            Ok(directory) => directory,\n            Err(open_error) => {\n                let metadata = fstatat(&directory, name.as_os_str(), AtFlags::AT_SYMLINK_NOFOLLOW);\n                if metadata.is_ok_and(|metadata| {\n                    SFlag::from_bits_truncate(metadata.st_mode).contains(SFlag::S_IFLNK)\n                }) {\n                    let parent = fstat(&directory)?;\n                    if parent.st_uid != 0 || parent.st_mode & 0o022 != 0 {\n                        bail!(\n                            \"refusing to follow symlink {} from an untrusted parent directory\",\n                            component_path.display()\n                        );\n                    }\n                    let target = nix::fcntl::readlinkat(&directory, name.as_os_str())?;\n                    let mut resolved = if Path::new(&target).is_absolute() {\n                        PathBuf::from(target)\n                    } else {\n                        current.join(target)\n                    };\n                    resolved.extend(components.iter().skip(index + 1));\n                    let resolved = resolved.absolutize()?.to_path_buf();\n                    if resolved == Path::new(\"/\") {\n                        bail!(\n                            \"refusing to resolve managed directory {} to the filesystem root\",\n                            path.display()\n                        );\n                    }","sourceCodeStart":1594,"sourceCodeEnd":1630,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/managed_files.rs#L1594-L1630","documentation":"A TOCTOU/symlink-planting defense: when the component walk encounters a symlink mid-path, it only follows it if the containing directory is owned by root and not group/other-writable. Otherwise it refuses, because an untrusted parent directory would let any local user swap the symlink target between check and use. This protects privilege-sensitive managed directory creation.","triggerScenarios":"Opening/creating a managed directory through a path where an intermediate component is a symlink living in a directory that is world- or group-writable, or not owned by uid 0 (checked via fstat of the parent directory).","commonSituations":"Managed paths under shared directories like /tmp or /var/tmp; symlink planted by another local user in a world-writable location; dotfile manager symlinks placed in user-owned group-writable directories; hardened umask changes making a previously-acceptable directory group-writable.","solutions":["Relocate the managed path under a root-owned, non-group-writable directory (e.g. /usr/local or /opt)","Tighten the parent directory permissions (`chmod go-w <parent>`) and ensure root ownership (`chown root <parent>`)","Remove the intermediate symlink and use a real directory instead","Audit for unexpected symlinks: `find <prefix> -type l`"],"exampleFix":"// before\nlrwxrwxrwx /tmp/tools/current -> /home/evil/tools  // parent /tmp is world-writable\n// after\n$ sudo mkdir -p /opt/tools && sudo ln -s /opt/tools-v1 /opt/tools/current\n$ sudo chmod 755 /opt/tools","handlingStrategy":"validation","validationCode":"fn parent_is_trusted(path: &Path) -> std::io::Result<bool> {\n    use std::os::unix::fs::MetadataExt;\n    if let Some(parent) = path.parent() {\n        let m = std::fs::metadata(parent)?;\n        return Ok(m.uid() == 0 && m.mode() & 0o022 == 0);\n    }\n    Ok(false)\n}","typeGuard":"fn safe_to_follow(path: &Path) -> bool {\n    use std::os::unix::fs::MetadataExt;\n    path.parent()\n        .and_then(|p| std::fs::metadata(p).ok())\n        .map(|m| m.uid() == 0 && m.mode() & 0o022 == 0)\n        .unwrap_or(false)\n}","tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"untrusted parent directory\") => {\n        fix_parent_ownership_and_permissions(path)?; // chown root, chmod go-w\n        retry();\n    }\n    Err(e) => return Err(e),\n    Ok(v) => v,\n}","preventionTips":["Keep managed prefixes under root-owned, non-group-writable directories","Avoid placing symlinks in /tmp or other world-writable dirs","Audit with find <prefix> -type l for unexpected links","Watch for umask changes that make parent dirs group-writable"],"tags":["security","symlink","permissions"],"backgroundTag":"path-traversal-blocked","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}