{"record":{"id":"3fc2d9f6606c6712","repo":"jdx/mise","slug":"brew-cask-refusing-operation-through-untrusted-di","errorCode":null,"errorMessage":"brew-cask: refusing operation through untrusted directory {}","messagePattern":"brew-cask: refusing operation through untrusted directory (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask.rs","lineNumber":2340,"sourceCode":"    let current_uid = nix::unistd::geteuid().as_raw();\n    let current_gid = nix::unistd::getegid().as_raw();\n    let current_groups = current_process_groups()?;\n    let sudo_uid = sudo_invoking_id(current_uid, \"SUDO_UID\");\n    let sudo_gid = sudo_invoking_id(current_uid, \"SUDO_GID\");\n    let verify = |fd: &std::os::fd::OwnedFd, directory: &Path| -> Result<()> {\n        let stat = fstat(fd)?;\n        let owner_is_user = stat.st_uid == current_uid || Some(stat.st_uid) == sudo_uid;\n        let trusted_owner = stat.st_uid == 0 || (allow_current_user && owner_is_user);\n        let trusted_group = stat.st_gid == current_gid\n            || Some(stat.st_gid) == sudo_gid\n            || current_groups.contains(&stat.st_gid);\n        let writable_by_untrusted = stat.st_mode & 0o002 != 0\n            || (stat.st_mode & 0o020 != 0 && (!allow_current_user || !trusted_group));\n        if !SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFDIR)\n            || !trusted_owner\n            || writable_by_untrusted\n        {\n            bail!(\n                \"brew-cask: refusing operation through untrusted directory {}\",\n                directory.display()\n            );\n        }\n        Ok(())\n    };\n    let mut directory = resolved_root.to_path_buf();\n    verify(&fd, &directory)?;\n    for component in relative.components() {\n        let Component::Normal(name) = component else {\n            bail!(\"brew-cask: invalid generic artifact parent\");\n        };\n        directory.push(name);\n        fd = match openat(&fd, name, flags, Mode::empty()) {\n            Ok(fd) => fd,\n            Err(nix::errno::Errno::ENOENT) if create_missing => {\n                match nix::sys::stat::mkdirat(\n                    &fd,","sourceCodeStart":2322,"sourceCodeEnd":2358,"githubUrl":"https://github.com/jdx/mise/blob/6f52dcdf99e282ef7a7db68c81301fa4618d0f79/src/system/packages/brew/cask.rs#L2322-L2358","documentation":"open_trusted_directory walks from the desymlinked Homebrew prefix to the artifact's parent one component at a time with O_NOFOLLOW and verifies every directory it opens. verify() rejects a component that is not a directory, is not owned by root (or by the invoking/sudo user when allow_current_user), or is writable by an untrusted party — world-writable (0o002), or group-writable (0o020) by a group that is neither the effective gid, a supplemental group, nor the sudo gid. This blocks symlink interposition and privilege escalation through mutable directories.","triggerScenarios":"A world-writable directory anywhere on the chain from the prefix to the artifact parent; a directory owned by another non-root user when allow_current_user is false; group-writable with an untrusted group (e.g. running unprivileged under a different account than the dir owner with SUDO_UID/GID not matching); a component that opened as a non-directory.","commonSituations":"Someone ran chmod 777 on /opt/homebrew/etc or a parent; prefix dirs left owned by a former admin account; mixing sudo and non-sudo installs so SUDO_UID doesn't match the dir owner; directories created by other package managers with loose group-write bits.","solutions":["Walk `ls -ld` from the Homebrew prefix down to the printed path to find the loose or foreign-owned directory","Fix it: sudo chown root:admin <dir> (or chown to the invoking user for per-user dirs) and sudo chmod go-w <dir>","Re-run the cask install","If a tool legitimately needs group write, make the group one the installing user belongs to so trusted_group passes"],"exampleFix":"# before\n$ ls -ld /opt/homebrew/etc\ndrwxrwxrwx  ... \n\n# after\n$ sudo chown root:admin /opt/homebrew/etc && sudo chmod go-w /opt/homebrew/etc","handlingStrategy":"validation","validationCode":"// Preflight: every component from the prefix down must be a trusted dir.\nfn prefix_chain_is_trusted(prefix: &std::path::Path) -> std::io::Result<()> {\n    use std::os::unix::fs::MetadataExt;\n    let uid = nix_uid(); // your effective uid\n    let mut cur = std::path::PathBuf::new();\n    for comp in prefix.components() {\n        cur.push(comp);\n        let md = std::fs::symlink_metadata(&cur)?;\n        if md.file_type().is_symlink() || !md.is_dir() { continue_or_fail()?; }\n        if !(md.uid() == 0 || md.uid() == uid) || md.mode() & 0o002 != 0 {\n            return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied,\n                format!(\"untrusted directory: {}\", cur.display())));\n        }\n    }\n    Ok(())\n}","typeGuard":"fn is_trusted_dir_owner(md: &std::fs::Metadata, euid: u32) -> bool {\n    use std::os::unix::fs::MetadataExt;\n    md.uid() == 0 || md.uid() == euid\n}","tryCatchPattern":null,"preventionTips":["Never chmod 777 anything under the Homebrew prefix","Audit ownership after admin account changes: chown -R root:admin (or user:admin) under the prefix","Decide once whether casks install elevated or as the user, and stay consistent so SUDO_UID checks line up"],"tags":["brew-cask","permissions","symlink-attack","homebrew-prefix","security"],"backgroundTag":"unsafe-directory-permissions","analyzedSha":"6f52dcdf99e282ef7a7db68c81301fa4618d0f79","analyzedAt":"2026-08-22T10:14:23.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}