# Verification Report — CVE-2026-20253 (SVD-2026-0603)

## Fix Summary

The proposed fix addresses the root cause of CVE-2026-20253 — missing authentication
(CWE-306) and unsanitized input on the Splunk Enterprise PostgreSQL sidecar recovery
endpoints — through a two-layer approach: (1) a **source-level patch** to the
reconstructed `postgres_recovery_manager.go` and `server.go` that enforces Splunk token
authentication on all `/v1/postgres/recovery/*` endpoints, sanitizes the `database`
parameter to reject libpq connection-string injection (`hostaddr=`, `passfile=`, `host=`,
`sslmode=`), validates `backupName` as a bare filename (no path separators or traversal),
and uses a fixed service account instead of sourcing the Postgres `-U` user from the
client `Authorization` header; and (2) a **deployable configuration mitigation**
(`[postgres] disabled = true` in `server.conf`) that disables the vulnerable sidecar
entirely on existing deployments that cannot immediately upgrade. The source-level patch
also closes the **authenticated variant bypass** identified by the variant analysis
(where the official 10.0.7 fix left the `database` parameter unsanitized, allowing an
authenticated attacker to achieve the same RCE chain).

## Changes Made

### 1. `postgres-service/internal/postgres/postgres_recovery_manager.go`
**Reconstructed from black-box behavioral analysis of `splunk/splunk:10.0.6` (closed-source).**

- **`sanitizeDatabase()`** (NEW): Validates that the `database` parameter is a simple
  PostgreSQL identifier (`^[A-Za-z_][A-Za-z0-9_]*$`). Rejects all libpq connection-string
  syntax — `hostaddr=<ip>`, `passfile=<path>`, `host=`, `sslmode=`, `dbname=`, spaces,
  `=` signs, and any non-identifier characters. This is the critical fix for both the
  original CVE and the authenticated variant bypass: the `database` field was previously
  passed verbatim as a libpq connection string, allowing the attacker to redirect
  `pg_dump` at an attacker-controlled Postgres and `pg_restore` at the local Splunk
  Postgres as the `postgres_admin` superuser.

- **`validateBackupName()`** (NEW): Validates that the backup file name is a bare
  filename (`^[A-Za-z0-9_][A-Za-z0-9_.-]*$`). Rejects path separators (`/`, `\`),
  parent-directory traversal (`..`), and absolute paths. The server constructs the full
  path under an approved backup directory, so the attacker can no longer choose an
  arbitrary on-disk path for `pg_dump -f` / `pg_restore`.

- **Fixed service account**: The `pg_dump`/`pg_restore` `-U` argument now uses a fixed
  service account (`m.pgUser`) instead of the client `Authorization` header username.
  The `ExtractUserFromAuthHeader()` function is removed entirely.

- **Schema split**: The shared `BackupRestoreRequest` (with `backupFile`) is replaced by
  `BackupRequest` and `RestoreRequest` (with validated `backupName`), matching the
  official 10.0.7 API schema change.

- **`backupCommand()` / `restoreCommand()`**: Now call `sanitizeDatabase()` and
  `validateBackupName()` before building the command line, returning an error on
  rejection. The file path is confined to the approved backup directory.

### 2. `postgres-service/api/server.go`
**Reconstructed from black-box behavioral analysis.**

- **`requireAuth()` middleware** (NEW): Enforces that all recovery endpoints
  (`/v1/postgres/recovery/backup`, `/restore`, `/status/*`, and
  `/service/info/specs/v1/openapi.json`) require a valid Splunk session token
  (`Authorization: Splunk <token>`). Unauthenticated and Basic-auth requests are
  rejected with `401 "Authorization header must use Splunk token"`. Invalid or expired
  tokens are rejected with `401 "Invalid or expired Splunk session token"`.

- **Handlers updated**: `handleBackup` and `handleRestore` now use the split
  `BackupRequest`/`RestoreRequest` schemas and rely on the recovery manager's internal
  validation (returns `400` on invalid `database`/`backupName`).

### 3. `opt/splunk/etc/system/local/server.conf` (NEW)
- **`[postgres] disabled = true`**: Deployable configuration mitigation that disables
  the PostgreSQL sidecar entirely. This is the official documented mitigation for
  deployments that cannot immediately upgrade to 10.0.7/10.2.4/10.4.0+.

## Verification Steps

### Part 1: Static Source Patch Verification

1. **Patch application**: The `proposed_fix.diff` was applied to the reconstructed
   vulnerable source tree with `patch -p1`. Result: **applies cleanly** (exit 0), all
   three files patched (`postgres_recovery_manager.go`, `server.go`, `server.conf`).

2. **Fix element validation** (11 checks, all PASS):
   - `sanitizeDatabase()` function present — rejects connection-string injection
   - Database validation uses strict regex (`^[A-Za-z_][A-Za-z0-9_]*$`)
   - `validateBackupName()` function present — rejects path separators
   - `backupName` validation rejects `/` and `\` (via `strings.ContainsAny`)
   - `requireAuth()` middleware present — enforces Splunk token authentication
   - Authentication rejects Basic-auth and missing credentials (401)
   - `ExtractUserFromAuthHeader` removed — fixed service account used for `-U`
   - Fixed service account (`m.pgUser`) used for `pg_dump`/`pg_restore` `-U`
   - Schema split: `BackupRequest` and `RestoreRequest` (backupFile → backupName)
   - `backupFile` field replaced by validated `backupName`
   - `server.conf` mitigation present (`[postgres] disabled = true`)

### Part 2: Runtime Mitigation Verification (Real Docker Product)

1. **Environment**: Started a fresh `splunk/splunk:10.0.6` container on a Docker network
   with an attacker `postgres:16-alpine` container. Waited for Splunk healthcheck
   (healthy) and sidecar endpoint registration.

2. **Pre-mitigation baseline**: Unauthenticated `POST .../recovery/backup` with
   `Authorization: Basic Og==` (empty credentials) returned **HTTP 400 "Failed to
   decode request"** — confirming the sidecar processed the request without
   authentication (the vulnerable behavior).

3. **Mitigation applied**: Added `[postgres] disabled = true` to
   `/opt/splunk/etc/system/local/server.conf` and restarted Splunk
   (`/opt/splunk/bin/splunk restart`).

4. **Post-mitigation verification**:
   - **Endpoint blocked**: Unauthenticated `POST .../recovery/backup` returned
     **HTTP 303 "See Other"** (instead of 400) — the sidecar is not registered and the
     proxy does not forward the request. **Mitigation effective.**
   - **Sidecar process stopped**: The PostgreSQL sidecar supervisor process
     (`pkg-run/pkg-postgres`) is not running after `disabled = true`.
   - **No arbitrary file created**: An unauthenticated backup request with
     `backupFile: /tmp/cve2026_verify_notruncate` was sent; the target file was **not
     created** — the file-operation primitive is blocked.

### Verification Output (from `verify_fix.sh`)

```
==========================================
  CVE-2026-20253 Fix Verification Summary
==========================================
  PART 1 (Static source patch):  PASS
  PART 2 (Runtime mitigation):   PASS
==========================================
[+] PASS: OVERALL: Fix verification PASSED
```

## Test Results

| Test | Description | Result |
|------|-------------|--------|
| Patch applies cleanly | `patch -p1 < proposed_fix.diff` | PASS (exit 0) |
| sanitizeDatabase present | Rejects `hostaddr=`/`passfile=` injection | PASS |
| validateBackupName present | Rejects path separators and traversal | PASS |
| requireAuth middleware | Enforces Splunk token auth (401 for Basic) | PASS |
| Fixed service account | `-U` from `m.pgUser`, not client header | PASS |
| Schema split | `BackupRequest`/`RestoreRequest` with `backupName` | PASS |
| server.conf mitigation | `[postgres] disabled = true` | PASS |
| Runtime: endpoint blocked | HTTP 303 after mitigation (was 400 before) | PASS |
| Runtime: sidecar stopped | No `pkg-postgres` process after restart | PASS |
| Runtime: no file created | `/tmp/...` not created after mitigation | PASS |

### Edge Cases Considered

- **`hostaddr=<attacker-ip>` injection**: Rejected by `sanitizeDatabase()` — the regex
  `^[A-Za-z_][A-Za-z0-9_]*$` does not match `hostaddr=... dbname=...`.
- **`passfile=<.pgpass>` injection**: Rejected by `sanitizeDatabase()` — same regex.
- **`../../tmp/foo` path traversal**: Rejected by `validateBackupName()` — the
  `ContainsAny(name, `/\`)` check and the `..` check prevent path control.
- **Absolute path `/tmp/foo`**: Rejected by `validateBackupName()` — leading `/` is a
  path separator.
- **Empty `Authorization` header**: Rejected by `requireAuth()` with 401.
- **Basic-auth credentials (`Authorization: Basic Og==`)**: Rejected by `requireAuth()`
  with 401 "Authorization header must use Splunk token".

## Remaining Concerns

1. **Closed-source product**: Splunk Enterprise is closed-source; the source-level patch
   is a reconstruction based on black-box behavioral analysis, not the actual vendor
   source. The patch demonstrates the correct fix approach for the vendor to implement.
   The deployable `server.conf` mitigation (`[postgres] disabled = true`) is the
   actionable fix for existing deployments and was verified on the real product.

2. **Configuration mitigation is blunt**: `[postgres] disabled = true` disables the
   entire PostgreSQL sidecar (including the local Splunk Postgres database), which may
   affect functionality. The source-level patch preserves functionality while closing
   the vulnerability. Deployments that need the sidecar should upgrade to 10.0.7+ and
   additionally ensure the `database` parameter is sanitized (the variant bypass).

3. **Variant bypass on 10.0.7**: The official 10.0.7 fix added authentication and
   `backupName` validation but did **not** sanitize the `database` parameter. An
   authenticated attacker can still inject `hostaddr=`/`passfile=` to achieve RCE on
   10.0.7. The `sanitizeDatabase()` function in this patch closes that gap. Deployments
   on 10.0.7 should apply the source-level fix or use the `disabled = true` mitigation
   until a version with database parameter sanitization is released.

4. **Read-only endpoints**: The fix enforces authentication on all endpoints including
   `/v1/postgres/recovery/status/*` and `/service/info/specs/v1/openapi.json`, which
   were information-disclosure vectors on both the vulnerable and fixed builds.

5. **Additional hardening recommendations**:
   - Restrict network access to Splunk Web (port 8000) and the sidecar port (8191).
   - Monitor for unexpected outbound Postgres connections (the backup stage makes Splunk
     initiate an outbound TCP connection to the attacker's database).
   - Monitor for unexpected modifications to files under `$SPLUNK_HOME/etc/apps/*/bin/`.
   - Add regression tests that recovery endpoints reject unauthenticated, Basic-auth,
     and connection-string-injected requests.
