{"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":null,"httpStatus":null,"severity":"error","filePath":"src/oci/builder.rs","lineNumber":1155,"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":1137,"sourceCodeEnd":1173,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/oci/builder.rs#L1137-L1173","documentation":"oci_target_path normalizes a [dotfiles] target into an in-image path: it must be absolute (checked earlier), have the leading '/' stripped, use forward slashes, and every path component must be non-empty and not '..'. If the resulting path is empty or contains a '..' component, the builder rejects it because '..' could escape the intended home directory inside the image (path traversal in the layer).","triggerScenarios":"build_dotfiles_layer with a [dotfiles] target like \"~/../etc/passwd\", \"/a/../b\", \"..\", or an empty target after stripping '/', producing a path with a '..' component or an empty string.","commonSituations":"Hand-written targets using relative escapes to reach system paths; templated targets where a variable resolves to '..' or empty; typos like a trailing '..' when attempting to install a file outside the home directory.","solutions":["Rewrite the target as an absolute path without '..' components, e.g. \"/etc/passwd\" instead of \"~/../etc/passwd\".","If a variable is templated into the target, validate/sanitize it (no '..') before rendering the config.","Fix empty targets — give each [dotfiles] entry a concrete absolute target path."],"exampleFix":"// before\n[dotfiles.\"~/../etc/motd\"]\nmode = \"content\"\ncontents = \"welcome\"\n\n// after\n[dotfiles.\"/etc/motd\"]\nmode = \"content\"\ncontents = \"welcome\"","handlingStrategy":"validation","validationCode":"// reject targets containing '..' or empty before config load\nlet t = target.trim_start_matches('~');\nif t.split('/').any(|c| c == \"..\") || t.is_empty() {\n    return Err(format!(\"unsafe dotfiles target: {target}\"));\n}","typeGuard":"fn is_safe_oci_target(target: &str) -> bool {\n    let p = target.strip_prefix('/').unwrap_or(target);\n    !p.is_empty()\n        && !p.split('/').any(|c| c.is_empty() || c == \".\" || c == \"..\")\n}","tryCatchPattern":null,"preventionTips":["Write dotfiles targets as absolute paths with no '..' components.","Sanitize variables interpolated into targets; never allow user input with '..'.","Keep all dotfiles inside the image home directory; use other mechanisms for system paths.","Add a config-lint rule rejecting '..' in [dotfiles] targets."],"tags":["oci","dotfiles","path","security"],"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"}