CVE-2026-20253 Runs as the splunk User — and That’s the Index You’d Hunt It In
Splunk’s 10.x line bundles a PostgreSQL storage sidecar — the data-management component that Edge Processor, OpAmp, and the SPL2 pipeline work depend on. CVE-2026-20253 is the bill for that decision. It’s a CVSS 9.8, CWE-306 missing-authentication bug: an unauthenticated caller reaches the sidecar’s recovery endpoints through SplunkWeb’s __raw relay and gets an arbitrary file create-or-truncate primitive, which researchers chained into remote code execution running as the Splunk service account. CISA added it to the KEV list on June 18, 2026 and set a June 21 remediation deadline for federal agencies under BOD 26-04. watchTowr published exploitation details on June 12 and released a non-exploit detection artifact generator — a vulnerability checker, not a weaponized PoC. Shadowserver was tracking north of 1,400 internet-exposed Splunk instances around disclosure — how many were on a vulnerable build, nobody’s saying.
Here’s the part that should bother you more than the CVSS. The thing getting popped is your detection platform, and the code runs as the user that owns _audit and _internal. The exploit and the evidence of the exploit live in the same trust boundary. That’s not a footnote. That’s the whole problem.
How the file-write actually happens
In affected 10.x deployments with the sidecar active, the Postgres process is local-only — watchTowr observed splunk-postgres listening on 127.0.0.1:5435 in its vulnerable test environment. Normally that’s unreachable from the network, and that’s the design assumption everyone leaned on. The problem is SplunkWeb will relay requests to it. The management plane exposes /en-US/splunkd/__raw/v1/postgres/recovery/backup and .../recovery/restore, and those handlers forward user-supplied values straight into the Postgres backup/restore tooling without authenticating the caller or sanitizing what gets passed down.
The backupFile and database parameters are attacker-controlled. Because the values land in libpq’s connection handling, an attacker can inject connection keywords — passfile=, hostaddr=, port=, dbname= — and steer the backend. One documented path targets /opt/splunk/var/packages/data/postgres/.pgpass, where Splunk stows the local Postgres credentials. From there the file-write primitive gets pointed at a script the Splunk service runs on its own schedule; the reported target is ssg_enable_modular_input.py under splunk_secure_gateway/bin. Replace the contents, wait for the scheduled invocation, and your code runs as splunk. I won’t walk the chain past that — the point of this piece is what it costs you, not how to build it.
Two scoping facts that decide whether you care. Splunk Enterprise 9.4 and earlier don’t have the sidecar, so they aren’t affected — this is a new-feature, new-surface story, and if you’re still on 9.x for unrelated reasons you dodged it. Splunk Cloud isn’t affected either, because the Cloud platform doesn’t run the Postgres sidecars. So the blast radius is on-prem Splunk Enterprise on 10.0.0–10.0.6 and 10.2.0–10.2.3. Anyone who jumped on the 10.x train early to get Edge Processor or SPL2 pipelines is exactly the population in scope. (That’s the recurring tax on being an early adopter of a bundled subsystem: you inherit its attack surface before the surface has been beaten on.)
The audit-integrity problem nobody puts on the slide
Code execution as the splunk OS account means the attacker owns the local Splunk filesystem and whatever indexed data, configs, secrets, and audit material live on that role. On an all-in-one or indexer, that includes the local buckets, _internal, and _audit — the exact logs you’d reach for to reconstruct what happened. On a search head it’s the local audit and internal logs, configs, apps, distributed-search credentials, and whatever administrative reach that node has into the rest of the deployment. Either way the attacker can tamper with local evidence and, depending on the role, damage or manipulate local indexed data. Forging a clean, internally consistent Splunk history is harder than creating or truncating a flat file — bucket rawdata, metadata, and tsidx see to that — but the forensic trust boundary is already broken. You’re asking a compromised box to honestly report its own compromise.
This is AU-9 (protection of audit information) in its purest, least-theoretical form. The control text says keep audit records out of reach of the people who could be subjects of an audit. On a popped SIEM, the malware is a subject of the audit and it’s running as the account that owns the records. If your only copy of _internal and _audit lives on the same box, your post-compromise timeline is whatever the attacker chose to leave you.
So the real question on the table isn’t “can I write a detection.” It’s “did I forward _internal and _audit to a separate indexer cluster — one I don’t administer from the same management plane — before any of this happened.” If yes, you have ground truth and the rest of this is tractable. If no, the detection work below still helps at request time, but your forensic floor is gone the moment the code runs. There’s no clever search that recovers it after the fact.
What the detection looks like at request time
The good news, such as it is: the exploit transits SplunkWeb before the box is owned, and that request gets logged at the moment it lands. So there’s a window where the evidence is still trustworthy, assuming you’ve shipped it off-box.
Start in _internal, but know its limit going in. The splunkd access logs record the HTTP request line and status — not the POST body — and the injected database and backupFile values ride in a JSON body, not the URL query string. So _internal tells you the endpoint was hit and from where; it does not contain the libpq keywords themselves. The request still lands in the access logs the moment it arrives:
index=_internal (sourcetype=splunkd_ui_access OR sourcetype=splunkd_access OR source="*splunkd_ui_access.log" OR source="*splunkd_access.log")
("/splunkd/__raw/v1/postgres/recovery/backup" OR "/splunkd/__raw/v1/postgres/recovery/restore")
| stats count min(_time) as firstSeen max(_time) as lastSeen values(status) as statuses values(method) as methods values(uri_path) as paths by host, clientip, user
The parenthesized sourcetype group plus the raw-string fallback both matter: field extractions vary across builds, and a bare sourcetype=a OR sourcetype=b sitting next to another bare term binds in ways you didn’t intend. Add method=POST if that field extracts reliably on your version.
The recovery endpoints are not something a normal interactive user or a normal app hits in day-to-day operation. On most deployments the baseline rate is zero. Any nonzero count from an unexpected source is worth a look — which makes this one of the rare detections where the threshold is genuinely “greater than zero” rather than a tuned floor. Treat any request to .../recovery/backup or /restore from a source with no business calling it as the primary Splunk-native signal.
To actually see the injected passfile, hostaddr, dbname, or backupFile values, you need telemetry that captures the request body or the resulting process — Splunk’s own logs won’t carry them. That means one of: a reverse proxy or WAF with request-body logging, NDR or packet capture downstream of TLS termination, or EDR capturing the command lines of the pg_dump/pg_restore children the write primitive spawns. If you run that proxy/WAF tier, it’s also where the truncation gotcha lives: long bodies and query strings get clipped by some pipelines — check the TRUNCATE setting in the relevant props.conf — and the injection markers are exactly the part that gets clipped, so an empty result there isn’t the same as a clean one.
Where the native rule gets noisy: if you actually use Edge Processor or the SPL2 pipelines, legitimate backup and restore operations exist and will hit these endpoints. Round one of tuning is separating real recovery traffic — known service accounts, internal management IPs, a predictable cadence — from the unauthenticated, parameter-injected variety. Build the allowlist of sources that have any business calling recovery endpoints, then alert on everything else. Expect the first week to surface your own backup automation and maybe a vuln scanner banging on __raw paths; carve those out by source and you’re left with a quiet, high-fidelity signal. And mind time skew between the SplunkWeb node and your off-box indexer; if NTP has drifted, correlating the web request against host-level telemetry turns into guesswork.
For the host layer, the file is the better artifact than the request. Watch the modular-input scripts under splunk_secure_gateway/bin with auditd on RHEL or your EDR’s file-integrity feature:
-w /opt/splunk/etc/apps/splunk_secure_gateway/bin/ -p wa -k splunk_ssg_write
The catch — and there’s always a catch with FIM on an application directory — is that app upgrades and Splunk’s own patching legitimately rewrite those files. So an auditd hit on ssg_enable_modular_input.py is signal only when it isn’t during a change window. Correlate the write event against your CM records; a modification with no corresponding change ticket and no package-manager parent process is the one that matters. On the process side, the thing you actually want is a python interpreter under the splunk_secure_gateway path spawning a shell or a network connection it has no business making — splunkd as the grandparent, an outbound socket as the result.
There’s also a Suricata/Snort rule in the ET set — SID 1.2069930.1, “Splunk Enterprise PostgreSQL Sidecar Service API via SplunkWeb Parameter Injection.” It keys on the request pattern. Treat it as a tripwire that tells you somebody tried, not as proof of success; it fires on the attempt, and on a SIEM front end behind a TLS-terminating reverse proxy it may not see the decrypted request at all unless you’re inspecting post-termination.
Patch, and the workaround that isn’t free
Fixed builds are 10.2.4, 10.0.7, and 10.4.0. Patch is the answer; SI-2 doesn’t have an asterisk here.
The workaround Splunk documents is to disable the sidecar:
[postgres]
disabled = true
Drop that in $SPLUNK_HOME/etc/system/local/server.conf and the unauthenticated surface goes away. But it takes Edge Processor, OpAmp, and SPL2 pipelines with it. Core search, indexing, and dashboards keep working. So the workaround is genuinely free only if you don’t use any of those features — in which case the honest move is to disable the sidecar permanently as a least-functionality measure (CM-7) rather than treating it as a temporary patch-window bridge. If you do use Edge Processor, the workaround is a service outage you’re choosing to take, and you should price it that way before you push it to a cluster at 0200.
Two more things while you’re in there. Get SplunkWeb and management (8089) off any segment that an untrusted network can reach — that’s SC-7, and a SIEM management plane reachable from a user VLAN was a problem before this CVE and will be a problem after the next one. And revisit what the splunk service account can actually touch on the host (AC-6); the RCE lands with whatever that account holds, and on too many installs it holds more than it needs.
| Control | What it’s doing here |
|---|---|
| SI-2 | Patch to 10.2.4 / 10.0.7 / 10.4.0 — primary remediation |
| AU-9 | Forward _internal and _audit off-box; the RCE owns them locally |
| SC-7 / AC-3 | Missing auth (CWE-306) on a network-relayed endpoint — restrict and mediate access to the SplunkWeb and management (8089) surface |
| CM-7 | Disable the Postgres sidecar if you don’t run Edge Processor / SPL2 |
| AC-6 | Constrain what the splunk service account can reach — that’s the RCE’s privilege |
| SR-3 | A bundled subsystem (Postgres) added attack surface the prior major version didn’t have — inventory and security-review components shipped with major-version feature additions |
The uncomfortable takeaway isn’t about Splunk specifically. It’s that the platform you point at everything else to find badness has the same property every other piece of software has — it ships features, features bring surface, and surface gets exploited. The difference is that when this one falls, it falls holding the evidence. Decide where your audit trail lives before the box is somebody else’s, because afterward is too late to decide anything.
Sources
- SVD-2026-0603: Unauthenticated Arbitrary File Creation and Truncation in a PostgreSQL Sidecar Service Endpoint in Splunk Enterprise (Splunk)
- Why Use App-Level Auth When Every Database Has Auth? — Splunk Enterprise CVE-2026-20253 Pre-Auth RCE (watchTowr Labs)
- watchTowr-vs-Splunk-CVE-2026-20253 detection artifact generator (watchTowr Labs, GitHub)
- Splunk CVE-2026-20253: Unauthenticated Remote Code Execution Vulnerability Explained — incl. Snort/ET rule SIDs (Picus Security)
- CISA: Splunk Enterprise flaw actively exploited, patch by Sunday (BleepingComputer)
- Unauthenticated RCE in Splunk Enterprise under active attack (CVE-2026-20253) (Help Net Security)
- Splunk Enterprise Vulnerability Exploited in Attacks Days After Disclosure (SecurityWeek)