Back to Blog
critical SEVERITY7 min read

Path Traversal Vulnerability Fixed in Hatch-Pet Scripts: A Deep Dive

A high-severity path traversal vulnerability was discovered and patched in the hatch-pet script suite, where unsanitized user input could allow attackers to read or overwrite sensitive files anywhere on the filesystem. The fix ensures that file paths are properly validated before use, preventing attackers from escaping the intended working directory. Understanding this class of vulnerability is essential for any developer working with file I/O and user-supplied input.

O
By Orbis AppSec
Published May 13, 2026Reviewed June 3, 2026

Answer Summary

This is a critical path traversal vulnerability (CWE-22) in the hatch-pet shell script suite, where unsanitized user input was passed directly into file path construction logic, allowing attackers to use sequences like `../../../etc/passwd` to escape the intended directory and access or overwrite arbitrary files on the filesystem. The fix resolves this by canonicalizing all user-supplied paths and verifying they remain within the permitted base directory before any file operation is performed. Developers can prevent this class of vulnerability by always resolving absolute paths and asserting the resolved path starts with the expected base directory prefix.

Vulnerability at a Glance

cweCWE-22
fixCanonicalize paths and verify they remain within the intended base directory before performing any file operation
riskAttackers can read or overwrite arbitrary files on the filesystem, including sensitive configuration and credential files
languageShell Script
root causeUser-supplied input used to construct file paths without canonicalization or boundary validation
vulnerabilityPath Traversal

Path Traversal Vulnerability Fixed in Hatch-Pet Scripts: A Deep Dive

Introduction

Imagine handing a stranger the keys to your house and saying, "You can only go in the kitchen." Now imagine they discover the keys also open every other room — and you never knew. That's essentially what a path traversal vulnerability does to your application's filesystem.

A path traversal vulnerability (also known as a directory traversal attack) allows an attacker to manipulate file path references in your application to access files and directories stored outside the intended folder. In the worst cases, this means reading sensitive configuration files, credentials, or even overwriting critical system files.

This post breaks down a recently patched high-severity path traversal vulnerability found in skills/hatch-pet/scripts/generate_pet_images.py, explains how it could have been exploited, and walks through the best practices you should adopt to prevent similar issues in your own code.


The Vulnerability Explained

What Happened?

The vulnerability resided in how the hatch-pet script constructed file paths. Specifically, the code joined a run_dir variable with a hardcoded filename (pet_request.json) to build a full file path — something like this:

# Vulnerable pattern
import os

run_dir = get_run_dir()  # Could come from CLI args, API params, or env vars
request_file = os.path.join(run_dir, 'pet_request.json')

with open(request_file, 'r') as f:
    data = f.read()

This looks innocent at first glance. The problem? run_dir was derived from user-controlled input without any validation.

The Anatomy of a Path Traversal Attack

The ../ sequence (or ..\ on Windows) is the key weapon in a path traversal attack. When you join an unvalidated user input with a base path, an attacker can supply sequences like:

../../etc/passwd
../../../var/secrets/.env
../../../../home/user/.ssh/id_rsa

So if an attacker passes run_dir as ../../etc/, the resulting path becomes:

os.path.join("../../etc/", "pet_request.json")
# Resolves to: ../../etc/pet_request.json

This effectively points the file operation outside the intended working directory, potentially exposing or corrupting sensitive system files.

Real-World Impact

Depending on how the vulnerable code is used, an attacker could:

  • Read sensitive files: /etc/passwd, .env files containing API keys, database credentials, private SSH keys
  • Overwrite critical files: Configuration files, log files, or even executable scripts
  • Achieve privilege escalation: By overwriting scripts run by higher-privileged processes
  • Exfiltrate application secrets: Internal configuration, tokens, or user data

Attack Scenario: Step by Step

Let's walk through a concrete attack scenario:

Setup: The hatch-pet script accepts a --run-dir CLI argument or reads RUN_DIR from an environment variable.

Step 1 — Attacker identifies the input vector:

python generate_pet_images.py --run-dir /tmp/myrun

Step 2 — Attacker crafts a malicious path:

python generate_pet_images.py --run-dir "../../etc/"

Step 3 — Script attempts to read:

# Internally resolves to:
open("../../etc/pet_request.json", 'r')
# Or in a write scenario:
open("../../etc/pet_request.json", 'w')

Step 4 — Data exfiltration or corruption occurs silently, with no error raised by the application.

This is classified as CWE-22: Improper Limitation of a Pathname to a Restricted Directory and is consistently listed in the OWASP Top 10 under A01:2021 – Broken Access Control.


The Fix

What Changed?

The fix introduces proper path validation before the run_dir variable is used to construct any file path. The core of the solution is to:

  1. Resolve the absolute, canonical path of the constructed file path
  2. Verify it falls within the expected base directory
  3. Reject any path that escapes the intended directory

Here's what a secure implementation looks like:

# BEFORE (Vulnerable)
import os

def get_request_file(run_dir):
    return os.path.join(run_dir, 'pet_request.json')

run_dir = args.run_dir  # Unvalidated user input
request_file = get_request_file(run_dir)

with open(request_file, 'r') as f:
    data = f.read()
# AFTER (Secure)
import os

# Define a strict base directory for all run operations
BASE_RUNS_DIR = os.path.abspath('/app/runs')

def get_safe_request_file(run_dir: str) -> str:
    """
    Safely construct a file path, ensuring it stays within BASE_RUNS_DIR.
    Raises ValueError if the resolved path escapes the base directory.
    """
    # Resolve the absolute path (resolves all ../ sequences)
    abs_run_dir = os.path.realpath(os.path.abspath(run_dir))

    # Ensure the resolved path starts with our allowed base directory
    if not abs_run_dir.startswith(BASE_RUNS_DIR + os.sep):
        raise ValueError(
            f"Invalid run_dir: path '{run_dir}' escapes the allowed directory."
        )

    return os.path.join(abs_run_dir, 'pet_request.json')

# Usage
try:
    request_file = get_safe_request_file(args.run_dir)
    with open(request_file, 'r') as f:
        data = f.read()
except ValueError as e:
    print(f"Security error: {e}")
    sys.exit(1)

Why This Fix Works

The critical line is:

abs_run_dir = os.path.realpath(os.path.abspath(run_dir))
  • os.path.abspath() converts the path to an absolute path based on the current working directory
  • os.path.realpath() resolves all symbolic links and normalizes ../ sequences

After this resolution, ../../etc/ becomes /etc/ — and the subsequent check:

if not abs_run_dir.startswith(BASE_RUNS_DIR + os.sep):

...immediately catches that /etc/ is not within /app/runs/ and raises an error before any file operation occurs.

Note: Always append os.sep when checking startswith() to avoid false positives where a directory like /app/runs-extra might incorrectly match a check for /app/runs.


Prevention & Best Practices

1. Always Validate and Canonicalize File Paths

Never trust user-supplied paths directly. Always resolve to an absolute, canonical path and verify it falls within your expected directory:

import os

def is_safe_path(base_dir: str, user_path: str) -> bool:
    base = os.path.realpath(os.path.abspath(base_dir))
    target = os.path.realpath(os.path.abspath(user_path))
    return target.startswith(base + os.sep)

2. Use Allowlists, Not Blocklists

Instead of trying to block ../ sequences (which can be bypassed with URL encoding like %2e%2e%2f), use an allowlist approach — define exactly what's permitted and reject everything else.

# BAD: Blocklist approach (bypassable)
if '../' in run_dir or '..\' in run_dir:
    raise ValueError("Invalid path")

# GOOD: Allowlist via canonical path check
if not is_safe_path(BASE_RUNS_DIR, run_dir):
    raise ValueError("Path escapes allowed directory")

3. Apply the Principle of Least Privilege

Your application should only have filesystem access to the directories it genuinely needs. Use OS-level controls:

  • Run processes as a dedicated low-privilege user
  • Use chroot jails or containers to limit filesystem visibility
  • Apply strict directory permissions so even a traversal can't access sensitive files

4. Treat All External Input as Untrusted

Path traversal inputs can come from many sources that developers often overlook:

  • ✅ CLI arguments
  • ✅ Environment variables
  • ✅ API request parameters
  • ✅ Configuration files loaded from external sources
  • ✅ Data read from databases or queues

Any of these can be attacker-controlled. Validate them all.

5. Use Security Scanning Tools

Integrate static analysis tools into your CI/CD pipeline to catch these issues before they reach production:

Tool Language Notes
Bandit Python Specifically detects path traversal patterns
Semgrep Multi-language Highly customizable rules
CodeQL Multi-language Deep semantic analysis
Snyk Code Multi-language Developer-friendly SAST

6. Write Security-Focused Tests

Add test cases that specifically attempt path traversal:

import pytest

def test_path_traversal_rejected():
    """Ensure path traversal attempts are blocked."""
    malicious_inputs = [
        "../../etc/passwd",
        "../secret",
        "/absolute/path/outside",
        "valid/../../../etc",
        "%2e%2e%2fetc",  # URL-encoded traversal
    ]
    for malicious_input in malicious_inputs:
        with pytest.raises(ValueError, match="escapes the allowed directory"):
            get_safe_request_file(malicious_input)

def test_valid_run_dir_accepted():
    """Ensure legitimate paths still work."""
    valid_input = "/app/runs/job-12345"
    result = get_safe_request_file(valid_input)
    assert result == "/app/runs/job-12345/pet_request.json"

Security Standards & References


Conclusion

Path traversal vulnerabilities are a classic yet persistently dangerous class of security flaw. They're often introduced through seemingly harmless code — a simple os.path.join() call that doesn't account for what a malicious actor might supply as input.

The key lessons from this vulnerability and its fix are:

  1. Never trust user input — validate and canonicalize all file paths before use
  2. Use os.path.realpath() + boundary checks to enforce directory restrictions in Python
  3. Allowlists beat blocklists — define what's permitted, reject everything else
  4. Least privilege matters — limit what your process can access at the OS level
  5. Automate detection — integrate SAST tools and write security-specific tests

Security vulnerabilities like this one are fixed one patch at a time, but the real win comes from building a culture where developers understand why these patterns are dangerous and proactively avoid them. The next time you write code that touches the filesystem with user-supplied input, take a moment to ask: "What happens if someone passes ../../etc/ here?"

That moment of reflection might just save your application — and your users.


This vulnerability was identified and patched as part of an automated security scanning process. Automated tools are a valuable layer of defense, but they work best when paired with developer education and secure-by-default coding practices.

Frequently Asked Questions

What is path traversal?

Path traversal (CWE-22) is a vulnerability where an attacker supplies input containing sequences like `../` to navigate outside the intended directory, accessing or modifying files the application should never touch.

How do you prevent path traversal in shell scripts?

Resolve the absolute canonical path of any user-supplied input using `realpath` or equivalent, then assert the result starts with the expected base directory prefix before performing any file operation.

What CWE is path traversal?

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

Is input length validation enough to prevent path traversal?

No. Length checks do not block `../` sequences. You must canonicalize the path and verify it falls within the permitted directory boundary.

Can static analysis detect path traversal?

Yes. Tools like Semgrep, Bandit, and ShellCheck can identify patterns where user-controlled variables flow into file operations without sanitization. Orbis AppSec detected this exact vulnerability automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1501

Related Articles

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.

critical

How buffer overflow via strcpy() happens in C Kconfig parsing and how to fix it

A critical buffer overflow vulnerability was discovered in the Linux kernel's Kconfig build system where `strcpy()` copied user-controlled symbol values into a fixed-size buffer without bounds checking. This flaw in `scripts/kconfig/symbol.c` could allow attackers to overwrite adjacent memory when processing malicious Kconfig files. The fix replaces the unsafe `strcpy()` with `memcpy()` using explicit length calculations.

medium

How insecure update manifest parsing happens in C++ UpdateHelper.cpp and how to fix it

TrafficMonitor's software update mechanism in `UpdateHelper.cpp` fetched and parsed update manifests from remote servers without validating the version string or enforcing trusted download URLs, leaving users exposed to man-in-the-middle (MITM) attacks. An attacker on the same network could intercept the update channel and inject a malicious binary under a crafted version string or an HTTP download link pointing to attacker-controlled infrastructure. The fix adds strict version-string sanitizati