{"record":{"id":"3e773a128746b0be","repo":"windmill-labs/windmill","slug":"path-is-outside-the-allowed-job-directory","errorCode":null,"errorMessage":"Path is outside the allowed job directory.","messagePattern":"Path is outside the allowed job directory\\.","errorType":"exception","errorClass":"std::io::Error (PermissionDenied)","httpStatus":null,"severity":"error","filePath":"backend/windmill-common/src/worker.rs","lineNumber":899,"sourceCode":"            Component::Normal(c) => {\n                ret.push(c);\n            }\n        }\n    }\n    ret\n}\n\npub fn is_allowed_file_location(job_dir: &str, user_defined_path: &str) -> error::Result<PathBuf> {\n    let job_dir = Path::new(job_dir);\n    let user_path = PathBuf::from(user_defined_path);\n\n    let full_path = job_dir.join(&user_path);\n\n    let normalized_job_dir = normalize_path(job_dir);\n    let normalized_full_path = normalize_path(&full_path);\n\n    if !normalized_full_path.starts_with(&normalized_job_dir) {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::PermissionDenied,\n            \"Path is outside the allowed job directory.\",\n        )\n        .into());\n    }\n\n    // The lexical check above cannot see symlinks: a symlink planted inside the\n    // job dir - e.g. by an earlier Ansible `git_repos` clone whose tracked\n    // content includes one - would let a later `git clone` or file write follow\n    // it out of the job dir while still passing the textual `starts_with` check.\n    // Walk the *normalized* relative path (`..`/`.` already collapsed) so each\n    // step matches the real on-disk resolution, and reject any existing component\n    // that is a symlink. Walking the raw user path would drift on an in-bounds\n    // `..` (e.g. `foo/../link`, which normalizes back inside the job dir) and miss\n    // the real symlinked component. Not-yet-existing components are safe: a path\n    // that does not exist cannot itself be a symlink.\n    let relative = normalized_full_path\n        .strip_prefix(&normalized_job_dir)","sourceCodeStart":881,"sourceCodeEnd":917,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-common/src/worker.rs#L881-L917","documentation":"`is_allowed_file_location` guards every user-supplied relative path used inside a job's dedicated directory. After joining the job dir with the user path and normalizing both, it rejects the path if the normalized result escapes the job directory. This blocks path-traversal attacks (e.g. `../../etc/passwd`) from writing outside the job sandbox.","triggerScenarios":"Calling write_file_at_user_defined_location (or the git clone/archive helpers that call this guard) with a relative path containing `..` segments that normalize outside the job directory, e.g. `../shared/file.txt` or an absolute path that resolves elsewhere.","commonSituations":"Scripts building paths from user input or concatenating flow inputs with `..`; symlinked or mounted job directories where normalization produces unexpected prefixes; hardcoded paths written for a different directory layout.","solutions":["Use a plain relative path that stays inside the job directory (e.g. `subdir/file.txt`) — no leading `/` and no `..` segments.","Compute the path relative to the job's working directory instead of an absolute filesystem path.","If you legitimately need files outside the job dir, use the appropriate Windmill resource/ storage (S3 object store) instead of filesystem paths.","Normalize your path before calling and verify it has no `..` components."],"exampleFix":"// before\nwrite_file_at_user_defined_location(job_dir, \"../outputs/result.txt\", data).await?;\n// after\nwrite_file_at_user_defined_location(job_dir, \"outputs/result.txt\", data).await?;","handlingStrategy":"validation","validationCode":"use std::path::{Component, Path};\nfn is_safe_relative(p: &str) -> bool {\n    let path = Path::new(p);\n    path.is_relative()\n        && !path.components().any(|c| matches!(c, Component::ParentDir | Component::RootDir | Component::Prefix(_)))\n}\nif !is_safe_relative(user_path) { return Err(\"path must be relative and stay in the job directory\"); }","typeGuard":"fn stays_in_job_dir(job_dir: &Path, user_path: &str) -> bool {\n    job_dir.join(user_path).canonicalize()\n        .map(|p| p.starts_with(job_dir))\n        .unwrap_or(false)\n}","tryCatchPattern":"match write_file_at_user_defined_location(&job_dir, &user_path, data).await {\n    Ok(()) => {},\n    Err(e) if e.to_string().contains(\"outside the allowed job directory\") => {\n        eprintln!(\"invalid destination path {user_path:?}: {e}\"); // fix path before retry\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never build paths from raw user input with `..` or absolute paths","Normalize paths and strip ParentDir components before passing them","Keep all job artifacts under the job working directory","Use Windmill S3 storage for anything that must outlive the job directory"],"tags":["path-traversal","security","filesystem","sandbox"],"backgroundTag":"path-outside-allowed-directory","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}