Back to Blog
high SEVERITY5 min read

How path traversal happens in Python open() and how to fix it

A high-severity path traversal vulnerability was discovered in `src/backend/snitch.py` where the `writeTestcase()` function accepted a user-controlled `portDir` parameter without sanitization. An attacker could craft malicious input like `../../etc` to write files outside the intended output directory. The fix implements path canonicalization using `pathlib.Path.resolve()` and validates that the final destination stays within the allowed base directory.

O
By Orbis AppSec
Published July 26, 2026Reviewed July 26, 2026

Answer Summary

Path traversal (CWE-22) in Python occurs when user-controlled input is concatenated into file paths passed to `open()` without sanitization. In this case, the `writeTestcase()` function in `snitch.py` allowed the `portDir` parameter to contain directory traversal sequences like `../`. The fix uses `os.path.basename()` to strip directory components, `pathlib.Path.resolve()` to canonicalize paths, and validates that the resolved path starts with the expected base directory before writing.

Vulnerability at a Glance

cweCWE-22
fixValidate resolved path stays within allowed base directory using pathlib
riskArbitrary file write to any location accessible by the application
languagePython
root causeUnsanitized portDir concatenated directly into file path
vulnerabilityPath Traversal via open()

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:

  1. portDir is concatenated directly into the path without stripping directory components
  2. index is 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

  1. os.path.basename(portDir) — Strips all directory components from portDir, so ../../etc/cron.d becomes just cron.d. This is the first line of defense.

  2. 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.

  3. Startswith validation — Even after the previous sanitization, the code explicitly verifies that destPath begins with basePath + os.sep. The os.sep suffix prevents a subtle bypass where basePath = "/var/app" would incorrectly match destPath = "/var/app_malicious".

  4. str(int(index)) — The index is now cast to int before string conversion, preventing any injection through the index parameter.

  5. mkdir(exist_ok=True) — Replaces the manual existence check with a cleaner, atomic operation that handles race conditions.

Key Takeaways

  • Never concatenate user input directly into file paths — The outputDirPath + "/" + portDir pattern in writeTestcase() is exactly what attackers look for
  • os.path.basename() alone isn't enough — Always combine it with path resolution and containment validation
  • The os.sep suffix in startswith checks is critical — Without it, /var/app would 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 portDir parameter passed to writeTestcase() function, originating from external input
  • Sink: open(destDir + "/pcap.data_packet." + str(index) + ".dat", "wb") in src/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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #120

Related Articles

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.