hashicorp/nomad · error

file path %q escapes capture directory %q

Error message

file path %q escapes capture directory %q

What it means

A sandbox/path-traversal guard in `writeBytes`: after joining the capture directory with the caller-supplied subdirectory and filename, Nomad checks `escapingfs.PathEscapesSandbox` and refuses to write if the resolved path would land outside the capture directory. This protects the debug archive from malicious or buggy inputs (e.g. filenames containing `..`) escaping to arbitrary filesystem locations.

Source

Thrown at command/operator_debug.go:1411

func (c *OperatorDebugCommand) writeBytes(dir, file string, data []byte) error {
	// Replace invalid characters in filename
	filename := helper.CleanFilename(file, "_")

	relativePath := filepath.Join(dir, filename)
	c.manifest = append(c.manifest, relativePath)
	dirPath := filepath.Join(c.collectDir, dir)
	filePath := filepath.Join(dirPath, filename)

	// Ensure parent directories exist
	err := escapingfs.EnsurePath(dirPath, true)
	if err != nil {
		return fmt.Errorf("failed to create parent directories of %q: %w", dirPath, err)
	}

	// Ensure filename doesn't escape the sandbox of the capture directory
	escapes := escapingfs.PathEscapesSandbox(c.collectDir, filePath)
	if escapes {
		return fmt.Errorf("file path %q escapes capture directory %q", filePath, c.collectDir)
	}

	// Create the file
	fh, err := os.Create(filePath)
	if err != nil {
		return fmt.Errorf("failed to create file %q, err: %w", filePath, err)
	}
	defer fh.Close()

	_, err = fh.Write(data)
	if err != nil {
		return fmt.Errorf("Failed to write data to file %q, err: %w", filePath, err)
	}
	return nil
}

// newFilePath returns a validated filepath rooted in the provided directory and
// path. It has been checked that it falls inside the sandbox and has been added

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Sanitize the filename/subdirectory before passing it: strip path separators and `..` sequences.
  2. Use filepath.Base on any externally-derived name so only the final component is used.
  3. Inspect the wrapped path in the error to see which component escaped and fix the caller generating it.
  4. If a legit symlink causes a false positive, remove the symlink and write within the capture dir directly.

Example fix

// before
name := node.Name // could be "../../etc/evil"
c.writeBytes(dir, name, resp, err)
// after
name := filepath.Base(strings.Map(func(r rune) rune {
    if r == '/' || r == os.PathSeparator { return '_' }
    return r
}, node.Name))
c.writeBytes(dir, name, resp, err)
Defensive patterns

Strategy: validation

Validate before calling

func safeName(name string) (string, error) {
    base := filepath.Base(name)
    if base == "." || base == ".." || base == "/" || strings.Contains(base, "..") {
        return "", fmt.Errorf("unsafe filename %q", name)
    }
    return base, nil
}

Prevention

When it happens

Trigger: Any writeBytes call where the resulting filePath resolves outside c.collectDir — typically a filename or dir argument containing `../` segments, symlinked paths, or an absolute path supplied as filename.

Common situations: Automated tooling generating capture filenames from untrusted input (node names, job names) that include path separators or `..`; a symlink inside collectDir pointing elsewhere.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/b7ca5ec10917e9ae. Report an issue: GitHub.