{"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":"error_code","errorClass":"eyre::Report","httpStatus":null,"severity":"warning","filePath":"src/system/managed_files.rs","lineNumber":1680,"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":1662,"sourceCodeEnd":1698,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/managed_files.rs#L1662-L1698","documentation":"This error is thrown when resolving a path inside mise's managed-file tracking encounters a symlink whose parent directory is untrusted. A symlink is only followed if the parent directory is owned by root and not group/world-writable (mode 0o022 clear); otherwise mise bails out rather than risk following an attacker-controlled symlink (TOCTOU hardening).","triggerScenarios":"A path component walked by open_or_create_directory_tree_inner is a symlink (AT_SYMLINK_NOFOLLOW detects S_IFLNK) and its parent directory's st_uid is not 0 or its mode has group/world write bits set.","commonSituations":"Running mise against a shared or user-owned cache/scratch directory where a component like tmp or cache has been replaced by a symlink; attacker-planted symlinks in a world-writable directory like /tmp; containers running as non-root with symlinks in HOME.","solutions":["Replace the symlink with a real directory (rm the symlink, mkdir the path)","Ensure the parent directory of the symlink is owned by root and not group/world-writable (chown root, chmod o-w,g-w)","Move the managed path to a location you control and mise trusts","Check for tools or scripts that create symlinks in your mise data directories"],"exampleFix":"// before\nls -la ~/.cache  # cache -> /tmp/shared-cache (symlink in user-owned parent)\n// after\nrm ~/.cache && mkdir ~/.cache  # real directory, error goes away","handlingStrategy":"validation","validationCode":"import os, stat\ndef symlink_parent_trusted(path):\n    parent = os.path.dirname(os.path.abspath(path))\n    st = os.stat(parent)\n    return st.st_uid == 0 and (st.st_mode & 0o022) == 0","typeGuard":"def is_symlink(p):\n    return os.path.islink(p)","tryCatchPattern":"match managed_dir.open(path) {\n    Err(e) if e.to_string().contains(\"untrusted parent directory\") => {\n        // replace symlink with real dir, then retry\n    }\n    r => r,\n}","preventionTips":["Do not place symlinks inside mise data/cache directories","Keep parent directories root-owned and not group/world-writable","Avoid running mise against paths under /tmp or shared writable dirs","Audit dotfile managers that replace directories with symlinks"],"tags":["filesystem","symlink","security","path-resolution"],"backgroundTag":"path-traversal-blocked","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}