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

Path traverses a symlink, which is not allowed.

Error message

Path traverses a symlink, which is not allowed.

What it means

During the same job-directory path validation, `is_allowed_file_location` walks each path component and checks `symlink_metadata` at every step. If any intermediate component is a symlink, the path could resolve outside the sandbox, so it is rejected. This closes the symlink-following variant of path traversal.

Source

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

    // 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)
        .unwrap_or(&normalized_full_path);
    let mut current = normalized_job_dir.clone();
    for component in relative.components() {
        if let Component::Normal(c) = component {
            current.push(c);
            if std::fs::symlink_metadata(&current)
                .map(|m| m.file_type().is_symlink())
                .unwrap_or(false)
            {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::PermissionDenied,
                    "Path traverses a symlink, which is not allowed.",
                )
                .into());
            }
        }
    }

    Ok(normalized_full_path)
}

pub fn write_file_at_user_defined_location(
    job_dir: &str,
    user_defined_path: &str,
    content: &str,
    mode: Option<u32>,
) -> error::Result<PathBuf> {
    let normalized_full_path = is_allowed_file_location(job_dir, user_defined_path)?;

View on GitHub (pinned to e474e8803c)

Solutions

  1. Remove/avoid symlinked intermediate directories inside the job directory; use real directories.
  2. Target a path with only regular directories and files.
  3. If a symlink is intentional, resolve the real path first and use one that stays inside the job dir without traversal.
  4. Check the job image/base setup for symlinks pre-created in the working directory.

Example fix

// before
write_file_at_user_defined_location(job_dir, "link_to_out/result.txt", data).await?;
// after
std::fs::remove_file("link_to_out"); // or use a real directory
write_file_at_user_defined_location(job_dir, "out/result.txt", data).await?;
Defensive patterns

Strategy: validation

Validate before calling

fn has_symlink_components(job_dir: &Path, rel: &str) -> bool {
    let mut current = job_dir.to_path_buf();
    for c in rel.split('/') {
        current.push(c);
        if std::fs::symlink_metadata(&current).map(|m| m.file_type().is_symlink()).unwrap_or(false) {
            return true;
        }
    }
    false
}
if has_symlink_components(&job_dir, user_path) { return Err("path must not traverse symlinks"); }

Type guard

fn path_is_symlink_free(job_dir: &Path, rel: &str) -> bool { !has_symlink_components(job_dir, rel) }

Try / catch

match write_file_at_user_defined_location(&job_dir, &user_path, data).await {
    Err(e) if e.to_string().contains("symlink") => {
        eprintln!("{user_path:?} traverses a symlink; use a real directory inside the job dir");
    }
    other => other?,
}

Prevention

When it happens

Trigger: A relative path whose intermediate directory (or final component checked during the walk) is a symlink — e.g. a script creating `link -> /etc` then writing `link/passwd` — passed to write_file_at_user_defined_location or the git clone helpers.

Common situations: Job directories that contain symlinks created by earlier steps or by the runtime image (e.g. `/tmp` symlinks, node_modules symlinks); reusing pre-created symlinked directories inside the job dir.

Related errors


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