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
- 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.
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
- 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
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
- Path traverses a symlink, which is not allowed.
- raw app path ${JSON.stringify(relPath)} escapes the app fold
- File resource path is invalid: {}
- bundle produced no javascript:\n${buildOutput}
- File not found: ${filePath}
AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03).
Data as JSON: /api/errors/3e773a128746b0be.
Report an issue: GitHub.