{"record":{"id":"641245a542f670b1","repo":"jdx/mise","slug":"path-must-not-escape-the-task-directory","errorCode":null,"errorMessage":"path must not escape the task directory","messagePattern":"path must not escape the task directory","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/task_cache.rs","lineNumber":1262,"sourceCode":"            )\n        })?;\n    }\n    build_output_matcher(root, &patterns)\n        .wrap_err_with(|| format!(\"task {} has an invalid cache output pattern\", task.name))?;\n    Ok(())\n}\n\nfn ensure_safe_relative(path: &Path) -> Result<()> {\n    if path.as_os_str().is_empty() || path.is_absolute() {\n        bail!(\"path must be a non-empty relative path\");\n    }\n    if path.components().any(|c| {\n        matches!(\n            c,\n            Component::ParentDir | Component::RootDir | Component::Prefix(_)\n        )\n    }) {\n        bail!(\"path must not escape the task directory\");\n    }\n    if !path.components().any(|c| matches!(c, Component::Normal(_))) {\n        bail!(\"path must identify an output beneath the task directory\");\n    }\n    Ok(())\n}\n\nfn resolve_output_roots(task: &Task, root: &Path, require_matches: bool) -> Result<Vec<PathBuf>> {\n    let mut resolved = BTreeSet::new();\n    let patterns = task.outputs.patterns();\n    let matcher = build_output_matcher(root, &patterns)?;\n    for output in output_glob_patterns(&patterns) {\n        ensure_safe_relative(Path::new(&output))?;\n        if crate::task::task_source_checker::is_glob_pattern(&output) {\n            let mut glob_matched = false;\n            for expanded in expand_enumeration_patterns(&output)? {\n                ensure_safe_relative(Path::new(&expanded))?;\n                for entry in glob(root.join(expanded).to_str().unwrap_or_default())? {","sourceCodeStart":1244,"sourceCodeEnd":1280,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/task_cache.rs#L1244-L1280","documentation":"ensure_safe_relative rejects output paths containing '..', a root component, or a Windows prefix. Such paths can resolve outside the task directory when the archive is packed or restored, so mise refuses them outright as a path-traversal guard.","triggerScenarios":"An outputs pattern resolves to a path whose components include ParentDir/RootDir/Prefix — e.g. outputs = ['../shared/dist'] or a glob that expands through a path containing '..'.","commonSituations":"Monorepos where outputs land in a sibling package's directory; escaping the project to a shared build dir; '../' segments left in a copied pattern.","solutions":["Point outputs at a path inside the project root, e.g. outputs = ['dist/']","Produce artifacts inside the root and add a separate copy task for consumers elsewhere","Remove '..' segments even where the filesystem would tolerate them"],"exampleFix":"# before\n[tasks.build]\noutputs = ['../shared/dist/app']\n\n# after\n[tasks.build]\noutputs = ['dist/app']","handlingStrategy":"validation","validationCode":"# CI / pre-commit: outputs never traverse upward\npython3 - <<'PY'\nimport tomllib, pathlib\ncfg = tomllib.loads(pathlib.Path('mise.toml').read_text())\nfor name, t in cfg.get('tasks', {}).items():\n    for o in t.get('outputs', []):\n        body = o.lstrip('!')\n        assert '..' not in pathlib.PurePosixPath(body).parts, f'{name}: output escapes root: {o}'\nPY","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never reference sibling or parent dirs from outputs; copy artifacts inside the root instead","The check fires on any '..' component, even where the filesystem would resolve it","Generate outputs in conventionally-created subdirectories"],"tags":["mise","task-cache","path-traversal","security"],"backgroundTag":"path-traversal","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}