{"record":{"id":"8b61388a2d837ad5","repo":"jdx/mise","slug":"refusing-cleanup-through-symlink-component","errorCode":null,"errorMessage":"refusing cleanup through symlink component {}","messagePattern":"refusing cleanup through symlink component (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/oci/packages.rs","lineNumber":562,"sourceCode":"    remove_path(&log)\n}\n\n/// Reject a cleanup target when it or any path component below `rootfs` is a\n/// symlink. OCI package layers are untrusted, so following one during cleanup\n/// could remove files outside the layer root.\nfn reject_symlink_components(rootfs: &Path, target: &Path) -> Result<()> {\n    let relative = target\n        .strip_prefix(rootfs)\n        .wrap_err_with(|| format!(\"cleanup target {} is outside rootfs\", target.display()))?;\n    let mut current = rootfs.to_path_buf();\n    for component in relative.components() {\n        let std::path::Component::Normal(component) = component else {\n            bail!(\"invalid cleanup target {}\", target.display());\n        };\n        current.push(component);\n        match fs::symlink_metadata(&current) {\n            Ok(metadata) if metadata.file_type().is_symlink() => {\n                bail!(\n                    \"refusing cleanup through symlink component {}\",\n                    current.display()\n                );\n            }\n            Ok(_) => {}\n            Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),\n            Err(err) => {\n                return Err(err)\n                    .wrap_err_with(|| format!(\"reading metadata for {}\", current.display()));\n            }\n        }\n    }\n    Ok(())\n}\n\n/// Remove every regular file under `dir` (recursively), leaving the directory\n/// structure and any symlinks intact. A missing `dir` is a no-op.\nfn remove_files_recursively(dir: &Path) -> Result<()> {","sourceCodeStart":544,"sourceCodeEnd":580,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/oci/packages.rs#L544-L580","documentation":"During apk transient cleanup, mise walks each path component of the target with symlink_metadata and refuses to continue if any component is itself a symlink. This prevents a planted or unexpected symlink from redirecting deletions outside the rootfs (TOCTOU-style escape).","triggerScenarios":"A directory component of an apk transient path inside the rootfs is a symlink when clean_apk_transients walks it — e.g. an image where /lib, /var/cache, or the apk db path is symlinked.","commonSituations":"Base images using symlinked directory layouts (common with merged-/usr or musl layouts); attacker-influenced layers; apk creating cache dirs as symlinks.","solutions":["Use a base image without symlinked components on the apk paths, or normalize the layout","Remove/repoint the offending symlink inside the rootfs before building","Skip custom transient cleanup if your image layout legitimately relies on symlinks"],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":"# check for symlinked components on apk paths in the base image\ndocker run --rm --entrypoint sh BASE_IMAGE -c 'for p in /etc/apk /lib/apk /lib/apk/db /var/cache/apk; do [ -L \"$p\" ] && echo \"symlink: $p\"; done; true'","typeGuard":null,"tryCatchPattern":"try {\n  buildOciImage(config);\n} catch (e) {\n  if (String(e.message).includes(\"refusing cleanup through symlink component\")) {\n    // flatten the symlinked layout in the base image or adjust it\n  } else throw e;\n}","preventionTips":["Choose base images without symlinked apk/db/cache directories","Audit base image layouts before adopting them","Treat this as a security stop — do not work around by disabling checks"],"tags":["oci","apk","security","symlink"],"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-14T05:17:10.506Z"}