Introduction
The writeTestcase() function in src/backend/snitch.py handles writing packet capture data to disk, creating per-port subdirectories to organize test case output. However, a critical flaw at line 745 allowed the portDir parameter to be concatenated directly into the file path without any validation:
destDir = outputDirPath + "/" + portDir
This seemingly innocent string concatenation created a high-severity path traversal vulnerability. Because portDir comes from external input and flows through to the open() call, an attacker could inject sequences like ../../ to escape the intended output directory and write arbitrary files anywhere the application has write permissions.
For developers building packet analysis tools or any application that writes user-organized files to disk, this pattern is a common pitfall that can have devastating consequences.
The Vulnerability Explained
Path traversal vulnerabilities occur when an application uses user-controlled input to construct file paths without proper sanitization. In this case, the vulnerable code in writeTestcase() looked like this:
def writeTestcase(data, outputDirPath, portDir, index):
destDir = outputDirPath + "/" + portDir
if not os.path.exists(destDir):
try:
os.mkdir(destDir)
except Exception:
print("[Worker] Could not create minor dir.")
with open(destDir + "/pcap.data_packet." + str(index) + ".dat", "wb") as out:
out.write(data)
The problem is twofold:
portDiris concatenated directly into the path without stripping directory componentsindexis converted to string without type validation, though this is a secondary concern
Attack Scenario
Imagine an attacker can influence the portDir value—perhaps through a network packet header, configuration file, or API parameter. They could supply:
portDir = "../../etc/cron.d"
With outputDirPath = "/var/app/output", the resulting path becomes:
/var/app/output/../../etc/cron.d/pcap.data_packet.0.dat
After path resolution, this writes to /etc/cron.d/pcap.data_packet.0.dat. If the application runs with elevated privileges, an attacker could:
- Overwrite configuration files to modify application behavior
- Plant cron jobs for persistent access
- Corrupt log files to hide malicious activity
- Write to web-accessible directories to deploy malicious scripts
Since this is a Node.js library (as noted in the threat model), the vulnerability affects all downstream consumers who integrate this package—amplifying the potential impact across multiple applications.
The Fix
The fix implements a defense-in-depth approach with three key changes:
Before (Vulnerable)
destDir = outputDirPath + "/" + portDir
if not os.path.exists(destDir):
try:
os.mkdir(destDir)
except Exception:
print("[Worker] Could not create minor dir.")
with open(destDir + "/pcap.data_packet." + str(index) + ".dat", "wb") as out:
out.write(data)
After (Fixed)
import pathlib
safePortDir = os.path.basename(portDir)
basePath = pathlib.Path(outputDirPath).resolve()
destPath = (basePath / safePortDir).resolve()
if not str(destPath).startswith(str(basePath) + os.sep):
raise ValueError("Path traversal detected in portDir")
destPath.mkdir(exist_ok=True)
filePath = destPath / ("pcap.data_packet." + str(int(index)) + ".dat")
with open(filePath, "wb") as out:
out.write(data)
What Changed and Why
-
os.path.basename(portDir)— Strips all directory components fromportDir, so../../etc/cron.dbecomes justcron.d. This is the first line of defense. -
pathlib.Path.resolve()— Canonicalizes both the base path and destination path, resolving any symbolic links and normalizing the path. This prevents attacks using symlinks or redundant separators. -
Startswith validation — Even after the previous sanitization, the code explicitly verifies that
destPathbegins withbasePath + os.sep. Theos.sepsuffix prevents a subtle bypass wherebasePath = "/var/app"would incorrectly matchdestPath = "/var/app_malicious". -
str(int(index))— The index is now cast tointbefore string conversion, preventing any injection through the index parameter. -
mkdir(exist_ok=True)— Replaces the manual existence check with a cleaner, atomic operation that handles race conditions.
Prevention & Best Practices
Secure File Path Construction
Always follow these principles when working with user-influenced file paths:
- Strip directory components first: Use
os.path.basename()to remove any path traversal sequences - Resolve to absolute paths: Use
pathlib.Path.resolve()to canonicalize paths - Validate containment: Always verify the final path stays within your allowed directory
- Use pathlib over string concatenation: The
/operator handles path separators correctly across platforms
Code Pattern to Follow
from pathlib import Path
import os
def safe_file_write(base_dir: str, user_filename: str, data: bytes):
# Strip directory components
safe_name = os.path.basename(user_filename)
# Resolve both paths
base = Path(base_dir).resolve()
target = (base / safe_name).resolve()
# Validate containment
if not str(target).startswith(str(base) + os.sep):
raise ValueError("Path traversal attempt detected")
# Safe to proceed
with open(target, "wb") as f:
f.write(data)
Detection Tools
- Semgrep: Use rules like
python.lang.security.audit.path-traversalto catch these patterns - Bandit: The
B108check flags hardcoded temp directories and related issues - CodeQL: Query for taint flow from user input to file operations
Key Takeaways
- Never concatenate user input directly into file paths — The
outputDirPath + "/" + portDirpattern inwriteTestcase()is exactly what attackers look for os.path.basename()alone isn't enough — Always combine it with path resolution and containment validation- The
os.sepsuffix in startswith checks is critical — Without it,/var/appwould match/var/app_malicious - Library vulnerabilities cascade downstream — Since this is a Node.js library, every consumer inherited this path traversal risk
- Validate all path components — The fix also added
int(index)casting to prevent injection through the index parameter
How Orbis AppSec Detected This
- Source: The
portDirparameter passed towriteTestcase()function, originating from external input - Sink:
open(destDir + "/pcap.data_packet." + str(index) + ".dat", "wb")insrc/backend/snitch.py:745 - Missing control: No sanitization of directory traversal sequences, no path canonicalization, no containment validation
- CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
- Fix: Added
os.path.basename()stripping,pathlib.Path.resolve()canonicalization, and startswith containment check before file operations
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 path traversal vulnerability in writeTestcase() demonstrates how a simple string concatenation can create a high-severity security flaw. The fix shows the proper pattern: strip directory components, resolve to canonical paths, and validate containment before any file operation.
When building applications that write files based on user input—whether it's port directories, uploaded filenames, or configuration paths—always assume the input is malicious. The few extra lines of validation code are trivial compared to the potential impact of arbitrary file writes.