windmill-labs/windmill · error · std::io::Error (PermissionDenied)

Path is outside the allowed job directory.

Error message

Path is outside the allowed job directory.

What it means

`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.

Source

Thrown at backend/windmill-common/src/worker.rs:899

            Component::Normal(c) => {
                ret.push(c);
            }
        }
    }
    ret
}

pub fn is_allowed_file_location(job_dir: &str, user_defined_path: &str) -> error::Result<PathBuf> {
    let job_dir = Path::new(job_dir);
    let user_path = PathBuf::from(user_defined_path);

    let full_path = job_dir.join(&user_path);

    let normalized_job_dir = normalize_path(job_dir);
    let normalized_full_path = normalize_path(&full_path);

    if !normalized_full_path.starts_with(&normalized_job_dir) {
        return Err(std::io::Error::new(
            std::io::ErrorKind::PermissionDenied,
            "Path is outside the allowed job directory.",
        )
        .into());
    }

    // The lexical check above cannot see symlinks: a symlink planted inside the
    // job dir - e.g. by an earlier Ansible `git_repos` clone whose tracked
    // content includes one - would let a later `git clone` or file write follow
    // it out of the job dir while still passing the textual `starts_with` check.
    // Walk the *normalized* relative path (`..`/`.` already collapsed) so each
    // step matches the real on-disk resolution, and reject any existing component
    // that is a symlink. Walking the raw user path would drift on an in-bounds
    // `..` (e.g. `foo/../link`, which normalizes back inside the job dir) and miss
    // the real symlinked component. Not-yet-existing components are safe: a path
    // that does not exist cannot itself be a symlink.
    let relative = normalized_full_path
        .strip_prefix(&normalized_job_dir)

View on GitHub (pinned to e474e8803c)

Solutions

  1. Use a plain relative path that stays inside the job directory (e.g. `subdir/file.txt`) — no leading `/` and no `..` segments.
  2. Compute the path relative to the job's working directory instead of an absolute filesystem path.
  3. If you legitimately need files outside the job dir, use the appropriate Windmill resource/ storage (S3 object store) instead of filesystem paths.
  4. Normalize your path before calling and verify it has no `..` components.

Example fix

// before
write_file_at_user_defined_location(job_dir, "../outputs/result.txt", data).await?;
// after
write_file_at_user_defined_location(job_dir, "outputs/result.txt", data).await?;
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Component, Path};
fn is_safe_relative(p: &str) -> bool {
    let path = Path::new(p);
    path.is_relative()
        && !path.components().any(|c| matches!(c, Component::ParentDir | Component::RootDir | Component::Prefix(_)))
}
if !is_safe_relative(user_path) { return Err("path must be relative and stay in the job directory"); }

Type guard

fn stays_in_job_dir(job_dir: &Path, user_path: &str) -> bool {
    job_dir.join(user_path).canonicalize()
        .map(|p| p.starts_with(job_dir))
        .unwrap_or(false)
}

Try / catch

match write_file_at_user_defined_location(&job_dir, &user_path, data).await {
    Ok(()) => {},
    Err(e) if e.to_string().contains("outside the allowed job directory") => {
        eprintln!("invalid destination path {user_path:?}: {e}"); // fix path before retry
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/3e773a128746b0be. Report an issue: GitHub.