{"record":{"id":"82025f9ffec2a3f8","repo":"jdx/mise","slug":"brew-cask-refusing-generic-artifact-source-outsid-82025f","errorCode":null,"errorMessage":"brew-cask: refusing generic artifact source outside the extraction root: {}","messagePattern":"brew-cask: refusing generic artifact source outside the extraction root: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/mod.rs","lineNumber":1772,"sourceCode":"        let Ok(relative) = source.strip_prefix(temporary_caskroom) else {\n            continue;\n        };\n        file::remove_file(&target)?;\n        create_flight_symlink(&final_caskroom.join(relative), &target, FlightSudo::Never)?;\n    }\n    Ok(())\n}\n\nfn install_generic_artifact(\n    stage: &Path,\n    temporary_caskroom: &Path,\n    artifact: &GenericArtifact,\n    targets: &mut FlightTargetTransaction,\n) -> Result<()> {\n    let source = find_artifact_matching(stage, &artifact.source, |_| true)\n        .ok_or_else(|| eyre!(\"brew-cask: artifact '{}' was not found\", artifact.source))?;\n    if !path_starts_with_resolved_root(&source, stage) {\n        bail!(\n            \"brew-cask: refusing generic artifact source outside the extraction root: {}\",\n            source.display()\n        );\n    }\n    let target = generic_artifact_target_path(&artifact.target)?;\n    // Not a lexical `strip_prefix`: the lookup resolves symlinks it had to\n    // traverse, so a source reached that way can be contained by the stage\n    // without sharing its literal prefix — as it is whenever `stage` itself\n    // has a symlinked ancestor. `staged_relative_path` retries against the\n    // resolved stage, matching the containment check above.\n    let relative_source = staged_relative_path(stage, &source).ok_or_else(|| {\n        eyre!(\n            \"brew-cask: generic artifact source is not contained by the extraction root: {}\",\n            source.display()\n        )\n    })?;\n    let caskroom_source = temporary_caskroom.join(relative_source);\n    if !path_starts_with_resolved_root(&caskroom_source, temporary_caskroom) {","sourceCodeStart":1754,"sourceCodeEnd":1790,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/packages/brew/cask/mod.rs#L1754-L1790","documentation":"For generic (non-app) artifacts, the staged source located by find_artifact_matching must resolve inside the extraction root (path_starts_with_resolved_root uses real paths, not lexical comparison). If the matched source resolves outside the stage — typically via symlinks in the archive pointing elsewhere — the manager refuses to install it, preventing files outside the sandbox from being linked into system locations.","triggerScenarios":"Installing a cask with a GenericArtifact whose resolved source (after symlink resolution by the artifact lookup) is not under the extraction root — e.g. the source pattern matched a symlink targeting a path outside the stage.","commonSituations":"Archives shipping symlinks to absolute paths or '../..' targets; malicious/tampered cask artifacts; generic artifacts whose glob accidentally matches an extractor-created escape symlink.","solutions":["Obtain the artifact from the trusted upstream tap and verify its sha256 before installing.","Rebuild/repackage the archive so all contents (including symlinks) live inside the extraction root with relative targets.","Adjust the artifact source pattern so it matches a real file inside the archive rather than an escaping symlink.","If intentional, restructure the cask to use preflight/postflight scripts instead of linking outside-root content."],"exampleFix":"// before (archive)\nbin/tool -> /usr/local/bin/tool\n// after\nbin/tool -> ../libexec/tool  # relative link inside archive","handlingStrategy":"validation","validationCode":"use std::path::Path;\nfn source_inside_root(root: &Path, source: &Path) -> bool {\n    source.canonicalize()\n        .ok()\n        .and_then(|r| r.strip_prefix(root.canonicalize().ok()?).ok())\n        .is_some()\n}","typeGuard":null,"tryCatchPattern":"match install_result {\n    Err(e) if e.to_string().contains(\"outside the extraction root\") => {\n        eprintln!(\"generic artifact source resolves outside the archive; repackage or use a trusted artifact\");\n    }\n    other => other?,\n}","preventionTips":["Check resolved (canonicalized) paths, not just lexical, before linking artifacts.","Reject archives containing symlinks that point outside the stage.","Verify cask artifact checksums from the tap before installation.","Keep generic artifact source globs narrow so they match real files, not escape symlinks."],"tags":["rust","brew","cask","symlink","path-traversal","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"}