Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) — see PR for fix commit |
| Ecosystem | N/A |
| CVE / GHSA | not assigned |
| CWE | CWE-22 (Path Traversal) |
The Vulnerability Explained
MDView's markitdown_bridge.py serves as a minimal offline bridge for converting DOCX files to Markdown. The script receives three command-line arguments via sys.argv[1:4]: a package directory, a source file path, and an output file path. The vulnerability lies in how these paths are used without any validation:
def main() -> None:
package_dir, source_path, output_path = sys.argv[1:4]
sys.path.insert(0, package_dir)
from markitdown import MarkItDown
md = MarkItDown()
result = md.convert_local(source_path)
The source_path flows directly into MarkItDown.convert_local(), which opens and reads the file. No normalization occurs, and no check ensures the path stays within intended boundaries.
An attacker controlling the command invocation could pass traversal sequences such as ../../../etc/passwd or ../../../sensitive_config.json as source_path. Since the script runs with whatever privileges the parent process holds, this enables reading arbitrary files from the filesystem. The output_path presents similar risks for unauthorized file writes, though the primary concern is information disclosure through the source path.
The Fix
The fix introduces three critical hardening measures using Python's os.path module:
def main() -> None:
package_dir, source_path, output_path = sys.argv[1:4]
package_dir = os.path.realpath(package_dir)
source_path = os.path.realpath(source_path)
output_path = os.path.realpath(output_path)
# Reject anything that isn't a real, existing file (blocks traversal via
# symlinks/"..") and refuse to overwrite a directory as the output.
if not os.path.isfile(source_path):
raise SystemExit(f"Invalid source path: {source_path}")
if os.path.isdir(output_path):
raise SystemExit(f"Invalid output path: {output_path}")
Normalization with os.path.realpath(): All three paths are resolved to their canonical absolute form, eliminating . and .. components and following symbolic links. This collapses traversal attempts before they reach the file operations.
Existence validation with os.path.isfile(): The source_path must point to an actual file, not a directory or non-existent path. This blocks traversal to directories and ensures the path resolved to something the attacker could legitimately target.
Type checking with os.path.isdir(): The output_path is rejected if it would overwrite a directory, preventing accidental or malicious directory corruption.
Key Takeaways
-
Command-line arguments are user input: Even
sys.argvvalues from a parent process can be attacker-controlled if that process passes through any user-derived data. Treat them with the same skepticism as HTTP parameters. -
os.path.realpath()overabspath()for security: When preventing traversal, symbolic links matter.realpath()resolves them, closing a bypass where an attacker uses a symlink to "teleport" outside the intended directory after normalization. -
Validate before importing: The fix validates paths before
sys.path.insert(0, package_dir)and themarkitdownimport. This ordering matters—an invalidpackage_dircould still affect Python's import path if checked too late. -
Fail with
SystemExitand descriptive messages: The fix usesraise SystemExit(...)rather than silent failures or exceptions, giving operators clear feedback about what validation failed.
How Orbis AppSec Detected This
Source: The sys.argv[1:4] slice accepting source_path and output_path from command-line arguments
Sink: MarkItDown.convert_local(source_path) for reading, and implicit file write operations via output_path
Missing control: No path normalization, no validation that resolved paths stay within intended directories, and no verification that source_path is a regular file
CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
Fix: Added os.path.realpath() normalization and explicit os.path.isfile()/os.path.isdir() checks to reject traversal attempts and ensure path types match expected usage
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
This vulnerability in markitdown_bridge.py demonstrates how even simple utility scripts can become security liabilities when they accept filesystem paths without validation. The fix's use of os.path.realpath() combined with type-specific checks provides a robust defense-in-depth approach: normalize first, verify existence and type second, and only then proceed with file operations. For developers building Tauri-based applications or any system that spawns helper processes, this pattern of validating paths at the process boundary is essential for maintaining security isolation.