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.

Prevention & Best Practices

Secure File Path Construction

Always follow these principles when working with user-influenced file paths:

  1. Strip directory components first: Use os.path.basename() to remove any path traversal sequences
  2. Resolve to absolute paths: Use pathlib.Path.resolve() to canonicalize paths
  3. Validate containment: Always verify the final path stays within your allowed directory
  4. 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-traversal to catch these patterns
  • Bandit: The B108 check 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 + "/" + 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.

References

Frequently Asked Questions

What is path traversal?

Path traversal is a vulnerability where attackers manipulate file paths using sequences like `../` to access or write files outside the intended directory, potentially reading sensitive data or overwriting critical files.

How do you prevent path traversal in Python?

Use `os.path.basename()` to strip directory components, resolve paths with `pathlib.Path.resolve()`, and validate the final path starts with your allowed base directory before any file operations.

What CWE is path traversal?

Path traversal is classified as CWE-22 (Improper Limitation of a Pathname to a Restricted Directory).

Is os.path.basename() enough to prevent path traversal?

No, `os.path.basename()` alone isn't sufficient because it doesn't handle all edge cases. You should also resolve the full path and verify it stays within your intended directory using a startswith check against the canonicalized base path.

Can static analysis detect path traversal?

Yes, static analysis tools like Semgrep can detect path traversal by tracing user-controlled input to file operation sinks like `open()`, `os.mkdir()`, or `shutil` functions without proper sanitization.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #120

Related Articles

critical

How Server-Side Request Forgery (SSRF) happens in JavaScript fetch() and how to fix it

A critical Server-Side Request Forgery vulnerability in `popup.js` allowed attackers to inject malicious URLs from scraped webpages directly into fetch() calls, potentially accessing internal network resources and AWS metadata endpoints. The fix adds URL validation to ensure only HTTP/HTTPS protocols are used and blocks requests to private IP ranges and localhost addresses.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in axios versions prior to 1.15.1 allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This meant HTTP requests intended to stay internal could be routed through an attacker-controlled proxy, potentially exposing sensitive data. The fix upgrades axios to version 1.15.1, which correctly validates URLs against NO_PROXY rules.

critical

How JSON request validation bypass happens in Node.js API handlers and how to fix it

A critical validation bypass in the `/api/agents/jobs` endpoint allowed attackers to send malformed JSON with arbitrary properties that could trigger prototype pollution or unexpected behavior. The fix added comprehensive validation checks including array detection and property existence verification to prevent malicious payloads from reaching downstream processing logic.

critical

How Server-Side Request Forgery happens in Node.js httpProxy.js and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `hubcmdui/routes/httpProxy.js` where the `/proxy/test` endpoint passed a user-controlled `testUrl` parameter directly to `axios.get()` without any validation. An attacker could exploit this to probe internal infrastructure — including AWS metadata endpoints, Redis instances, and private network ranges. The fix introduces a strict URL validation function that blocks private IP ranges, loopback addresses, and non-HTTP(S)

critical

How Remote Configuration Injection happens in JavaScript fetch() and how to fix it

A critical vulnerability in `js/config.js` allowed attackers to inject malicious configuration data through DNS spoofing or man-in-the-middle attacks. The application fetched remote JSON configuration from GitHub without validating the response structure, enabling arbitrary code execution through crafted payloads. A single validation check now ensures only properly-formed configuration objects are accepted.

high

How missing Dependabot cooldown configuration happens in GitHub Actions and how to fix it

A high-severity vulnerability was discovered in the `.github/dependabot.yml` configuration file where no cooldown period was set for dependency updates. This exposed the Node.js library and its downstream consumers to potentially malicious or unstable packages immediately after publication. The fix adds a 7-day cooldown period to all package ecosystem entries, providing a critical safety buffer before accepting newly published package versions.