# Patch Analysis — CVE-2026-20253 (SVD-2026-0603)

## Overview

The fix for CVE-2026-20253 was applied in Splunk Enterprise 10.0.7 (also 10.2.4, 10.4.0+).
Splunk Enterprise is closed-source; this analysis is based on behavioral testing of the official
`splunk/splunk:10.0.7` Docker image compared with the vulnerable `splunk/splunk:10.0.6` image,
supplemented by inspection of configuration files and sidecar logs accessible within the containers.

## What the Fix Changes

### 1. Authentication Enforcement (Proxy + Sidecar)

**Before (10.0.6):** The PostgreSQL sidecar recovery endpoints
(`/v1/postgres/recovery/backup`, `/v1/postgres/recovery/restore`) were accessible without
authentication. The `Authorization` header's Basic-auth username was used verbatim as the Postgres
`-U` user, but no authentication check was performed — empty/garbage credentials were accepted.

**After (10.0.7):** Both the Splunk Web proxy (port 8000) and the sidecar binary itself
(`postgres_nanny` on localhost) enforce authentication:
- Unauthenticated requests to backup/restore return `401 "Authorization header must use Splunk token"`.
- Requests with an invalid Splunk token return `401 "Invalid or expired Splunk session token"`.
- The sidecar binary enforces this independently — direct access on localhost:39411 also returns 401.

**Config files:** The `restmap.conf` and `web.conf` expose stanzas are **identical** between versions
(`requireAuthentication = false` on all postgres endpoints in both). The auth enforcement is in the
sidecar binary code, not in the config files. The `manifest.json` is also identical between versions
(all endpoints have `requireAuthentication: false`). The top-level `restmap.conf` default changed from
`requireAuthentication = true` (vulnerable) to `requireAuthentication = false` (fixed), but this is
the default for ALL endpoints and the postgres stanzas override it to `false` in both versions.

### 2. API Schema Change: backupFile → backupName

**Before (10.0.6):** The `BackupRestoreRequest` schema had a `backupFile` field described as "The
absolute filepath of the backup being created or used for a backup/restore operation." This allowed
the attacker to choose any file path on the Splunk server for `pg_dump -f` / `pg_restore` to
create/truncate.

**After (10.0.7):** The schema was split into `BackupRequest` and `RestoreRequest`:
- `BackupRequest.backupName`: "The filename (not a path) for the backup file. Optional; if omitted the
  server generates a name."
- `RestoreRequest.backupName`: "The filename (not a path) of the backup to restore from. Must match
  the backupName returned by a prior backup operation."

**Server-side validation:** Requests with path separators in `backupName` (e.g., `../../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`.

**Backup directory containment:** Backup files are written to
`/opt/splunk/var/packages/data/postgres/db/backups/` with server-generated names like
`<database>-<timestamp>-<uuid>.dump`.

### 3. Package Version Change

- Vulnerable: postgres package `fe4b8a9f-20260316t134658`
- Fixed: postgres package `8a372c04-20260422t100430`

The `postgres` binary (65 MB Go binary at `pkg-postgres841090839/postgres`) was updated between
versions. The sidecar source is `postgres-service/internal/postgres/postgres_recovery_manager.go`
(referenced in log messages).

## What the Fix Assumes

The fix assumes that:
1. **Authentication is sufficient** — Once auth is enforced, an authenticated user calling the
   endpoints with a legitimate `database` value cannot achieve arbitrary file write.
2. **`backupName` validation prevents path control** — By restricting the file parameter to a
   filename (not a path), the attacker cannot control where files are written.
3. **The `database` parameter is a simple database name** — The fix does not validate or sanitize
   the `database` parameter, assuming it will be a local database name like `template1`.

## What the Fix Does NOT Cover (Gaps)

### Gap 1: `database` Parameter Not Sanitized (CRITICAL — enables the bypass)

The `database` parameter is passed verbatim as a libpq connection string to `pg_dump`/`pg_restore`.
The fix did not add any validation or sanitization. An authenticated attacker can inject:

- **`hostaddr=<attacker-ip>`** — Overrides the default `-h localhost`, redirecting `pg_dump` to an
  attacker-controlled Postgres. Confirmed: `BackupComplete` on 10.0.7 when the attacker PG has the
  expected role.
- **`passfile=<.pgpass>`** — Specifies a credential file, allowing `pg_restore` to authenticate as
  the `postgres_admin` superuser against the local Splunk Postgres. Confirmed: `RestoreComplete` on
  10.0.7.
- **`dbname=<name>`** — Selects which database to connect to.

This is the same root cause as the original CVE. The fix addressed the file PATH (`backupFile` →
`backupName`) but not the CONNECTION STRING (`database`), which is the other half of the attack
chain.

### Gap 2: `lo_export()` Target Path in Database Content

The `lo_export()` call that writes attacker bytes to an arbitrary path is embedded in the attacker's
PL/pgSQL function (inside the dumped database content), not in the API parameter. The `backupName`
validation prevents the attacker from controlling the backup file path through the API, but the
`lo_export()` target path is in the database content loaded during `pg_restore`. The fix does not
prevent this because it doesn't inspect or restrict database content.

### Gap 3: Read-Only Endpoints Bypass Auth (Information Disclosure)

The following endpoints are accessible without authentication on the fixed build:
- `GET /v1/postgres/recovery/status/*` → 500 (reaches sidecar, not 401)
- `GET /service/info/specs/v1/openapi.json` → 200 (full API spec leaked)
- `GET /v1/postgres/telemetry` → 200 (PostgreSQL query results leaked)
- `GET /v1/postgres/health` → 200 (sidecar health status)

These are not in the web.conf `[expose:...]` stanzas for telemetry/health (they return 303 through
the proxy), but status and openapi ARE exposed and bypass the auth check. The telemetry endpoint is
directly accessible on the sidecar's localhost port. These are information-disclosure issues.

### Gap 4: `-U` User Still Derived from Auth Context

The `pg_dump`/`pg_restore` `-U` user is still derived from the authentication context (the fixed
build uses `postgres_admin` as the default user). This means the attacker can control which Postgres
user is used for the connection. The fix should use a fixed service account.

## Comparison of Behavior Before and After the Fix

| Aspect | Vulnerable (10.0.6) | Fixed (10.0.7) |
|--------|---------------------|----------------|
| Auth on backup/restore | None (accepts empty Basic) | Splunk token required (401 without) |
| `backupFile` parameter | Accepted (absolute path) | Ignored (server generates name) |
| `backupName` parameter | N/A (uses `backupFile`) | Validated (filename only, no path) |
| `database` parameter | Raw libpq conn string | Raw libpq conn string (NOT sanitized) |
| `hostaddr=` injection | Works (unauthenticated) | Works (authenticated) |
| `passfile=` injection | Works (unauthenticated) | Works (authenticated) |
| `lo_export()` via CHECK | Fires during restore | Fires during restore (same) |
| File write to arbitrary path | Via `backupFile` + `lo_export` | Via `lo_export` only (path in DB content) |
| RCE as splunk user | Yes (unauthenticated) | Yes (authenticated) |
| Status endpoint auth | None | None (500, not 401) |
| OpenAPI spec auth | None | None (200, not 401) |
| Telemetry auth | None | None (200, leaks PG data) |

## Fix Completeness Assessment

**The fix is INCOMPLETE.** It addresses two of the three attacker-controlled inputs:
- ✅ `backupFile` → `backupName` (path → filename, validated)
- ✅ Authentication on backup/restore
- ❌ `database` connection string — NOT sanitized (enables the bypass)

The fix provides defense-in-depth (auth + path validation) but leaves the `database` parameter
completely unsanitized, allowing the same `hostaddr=`/`passfile=` injection that powered the
original CVE. An authenticated attacker can still achieve arbitrary file write and RCE via the same
`lo_export()` mechanism.

## Target Threat Model Scope

Splunk Enterprise's security documentation was not explicitly found in the container (no
`SECURITY.md`). However, the CVE description and the fix behavior indicate that:
- Unauthenticated network access to the PostgreSQL sidecar endpoints is considered a vulnerability
  (CWE-306: Missing Authentication for Critical Function)
- The fix enforces authentication, acknowledging that these endpoints should not be accessible
  without credentials
- The `database` parameter injection allowing authenticated users to redirect `pg_dump`/`pg_restore`
  to external hosts and inject `passfile=` is clearly outside the intended behavior — the endpoints
  are designed for local database backup/restore, not for connecting to arbitrary external Postgres
  instances or using arbitrary credential files

The variant bypass is valid within the target's security scope: an authenticated user should not be
able to achieve arbitrary file write and RCE through the backup/restore endpoints.
