{"record":{"id":"5718d76a30f6f007","repo":"jdx/mise","slug":"dotfiles-target-is-not-a-safe-oci-path","errorCode":null,"errorMessage":"[dotfiles].\"{}\": target is not a safe OCI path","messagePattern":"\\[dotfiles\\]\\.\"(.+?)\": target is not a safe OCI path","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/oci/builder.rs","lineNumber":1031,"sourceCode":"        (files, dirs)\n    }\n}\n\nfn oci_target_path(req: &FileRequest) -> Result<String> {\n    let raw = req.target_raw.as_str();\n    let path = if raw == \"~\" {\n        \"root\".to_string()\n    } else if let Some(rest) = raw.strip_prefix(\"~/\") {\n        format!(\"root/{rest}\")\n    } else {\n        req.target\n            .strip_prefix(\"/\")\n            .map_err(|_| eyre::eyre!(\"dotfile target must be absolute: {}\", req.target_raw))?\n            .to_string_lossy()\n            .replace('\\\\', \"/\")\n    };\n    if path.is_empty() || path.split('/').any(|p| p == \"..\") {\n        bail!(\n            \"[dotfiles].\\\"{}\\\": target is not a safe OCI path\",\n            req.target_raw\n        );\n    }\n    Ok(path)\n}\n\nfn source_mode(path: &std::path::Path) -> Result<u32> {\n    let md = path.metadata()?;\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::PermissionsExt;\n        Ok(md.permissions().mode() & 0o7777)\n    }\n    #[cfg(not(unix))]\n    {\n        let _ = md;\n        Ok(0o644)","sourceCodeStart":1013,"sourceCodeEnd":1049,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/oci/builder.rs#L1013-L1049","documentation":"oci_target_path normalizes a [dotfiles] target (~ -> root, ~/x -> root/x, absolute /x -> x, backslashes to slashes) and then rejects the result if it is empty or contains a '..' component. This is a safety gate: '..' in the image path would escape the intended location inside the container filesystem, and an empty path has no tar meaning.","triggerScenarios":"A [dotfiles] target like \"/../etc/passwd\", \"~/../..\", \"a/../../b\", or a bare \"/\" (which strips to the empty string). Windows-style separators are converted before the check, so \"~\\..\\x\" is caught too.","commonSituations":"Copy-pasted relative targets from another tool, targets built by string concatenation that accidentally include '..', or a target of just \"/\" expecting the image root.","solutions":["Rewrite the target as an absolute path without any '..' segment, e.g. \"/etc/mise/config.toml\" or \"~/.config/mise/config.toml\".","Replace a \"/\" target with the concrete destination file name under a real directory.","If you need path joining, build it from components and reject '..' programmatically instead of concatenating strings."],"exampleFix":"# before\n[dotfiles.\"~/secrets\"]\ntarget = \"/../etc/passwd\"\n\n# after\n[dotfiles.\"~/secrets\"]\ntarget = \"/etc/mise/secrets\"","handlingStrategy":"validation","validationCode":"def safe_oci_target(t: str) -> bool:\n    if t == \"~\":\n        return True\n    if t.startswith(\"~/\"):\n        p = \"root/\" + t[2:]\n    elif t.startswith(\"/\"):\n        p = t[1:]\n    else:\n        return False  # relative -> different error, still reject up front\n    p = p.replace(\"\\\\\", \"/\")\n    return bool(p) and all(part != \"..\" for part in p.split(\"/\"))\n\nassert all(safe_oci_target(t) for t in targets)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always write targets as absolute paths or ~/ relative, never relative or containing '..'.","If targets come from templates, lint them for '..' segments before running mise oci build.","Remember backslashes are normalized to '/', so Windows-style escapes do not bypass the check."],"tags":["oci","dotfiles","path-traversal","validation"],"backgroundTag":"path-traversal-validation","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}