CVE-2026-78155: How an Untrusted Search Path Let StackGres Tenants Become Admins

A critical CWE-426 flaw in the StackGres Kubernetes operator shows how untrusted search paths let low-privileged tenants escalate to admin — here's how the same mistake shows up in your own subprocess code, and how to fix it.

On August 23, 2026, a CVSS 9.9 vulnerability landed in the StackGres PostgreSQL operator for Kubernetes: CVE-2026-78155, tracked under CWE-426, Untrusted Search Path. The bug lets a low-privilege tenant who merely owns a database escalate to full operator-admin privileges, over the network, with no user interaction required. It’s a reminder that a whole class of privilege-escalation bugs has nothing to do with SQL injection or broken auth tokens — it’s about how your code finds the binaries and libraries it runs.

What “untrusted search path” actually means

CWE-426 happens when a privileged process resolves the location of an executable, script, or library using a search mechanism (an environment variable like PATH, the current working directory, a plugin directory, a classpath) that a lower-privileged actor can influence. If that actor can plant a file earlier in the search order, the privileged process ends up running attacker-controlled code with its own elevated permissions. MITRE’s canonical writeup covers the general pattern; OWASP maps CWE-426 to A08:2021 — Software and Data Integrity Failures, because at its core the failure is trusting an unverified source for code that’s about to execute.

In a Kubernetes operator like StackGres, this pattern is especially dangerous: the operator runs with cluster-wide privileges, while tenant workloads run in the same node pool or share init containers, sidecars, or volumes that can influence what ends up on PATH or in a lookup directory. If the operator shells out to a helper binary by name instead of a verified absolute path, a tenant who controls any writable location that gets searched first effectively controls what the operator executes as admin.

The vulnerable pattern

Here’s the shape of the bug, stripped to its essentials, in Python:

import subprocess

# VULNERABLE: relies on the runtime PATH to locate the binary.
def run_pg_dump(db_name: str) -> None:
    subprocess.run(["pg_dump", db_name, "-f", f"/backups/{db_name}.sql"])

subprocess.run with a bare command name does a PATH lookup at call time. If a tenant-writable directory sits anywhere in that PATH — a common mistake in shared containers or when PATH is inherited from a less-trusted parent process — they can drop their own pg_dump script there and it runs with whatever privileges the operator has.

The same class of bug in Go, closer to how a Kubernetes operator like StackGres is structured:

// VULNERABLE: exec.Command resolves an unqualified name via $PATH.
func runMigration(dbName string) error {
    cmd := exec.Command("pg-migrate", "--db", dbName)
    return cmd.Run()
}

exec.Command silently calls the equivalent of LookPath whenever the first argument isn’t already an absolute path — walking every directory in PATH, including any a tenant-controlled init container may have prepended via a shared volume or environment override.

The fix: pin the path, don’t search for it

import os
import subprocess

TRUSTED_PG_DUMP = "/usr/lib/postgresql/16/bin/pg_dump"

def run_pg_dump(db_name: str) -> None:
    st = os.stat(TRUSTED_PG_DUMP)
    if st.st_uid != 0 or (st.st_mode & 0o022):
        raise RuntimeError("pg_dump has unsafe ownership or permissions")
    subprocess.run(
        [TRUSTED_PG_DUMP, db_name, "-f", f"/backups/{db_name}.sql"],
        check=True,
        env={"PATH": "/usr/bin:/bin"},
    )
const trustedMigrateBinary = "/opt/stackgres/bin/pg-migrate"

// FIXED: absolute path baked into the image, ownership never re-resolved
// from a tenant-influenced PATH.
func runMigration(dbName string) error {
    info, err := os.Stat(trustedMigrateBinary)
    if err != nil || info.Mode()&0o022 != 0 {
        return errors.New("migration binary missing or has unsafe permissions")
    }
    cmd := exec.Command(trustedMigrateBinary, "--db", dbName)
    cmd.Env = []string{"PATH=/usr/bin:/bin"}
    return cmd.Run()
}

Three changes matter here: the binary is referenced by an absolute path baked into the trusted image rather than resolved at runtime; the process explicitly sets a minimal, hardcoded PATH instead of inheriting one that a tenant or sidecar could have polluted; and ownership/permission bits are checked before execution so a world-writable or non-root-owned file is rejected outright.

Checklist for your own code

  • Never call exec, subprocess, child_process.exec, or ProcessBuilder with a bare command name in privileged code paths — always resolve to an absolute path at build or startup time, not per-request.
  • Set an explicit, minimal PATH (and equivalent: LD_LIBRARY_PATH, Python’s sys.path, Java classpath, Node’s module resolution) for any process that runs with elevated privileges; don’t inherit environment from less-trusted callers.
  • Verify ownership and write permissions on binaries and shared libraries before invoking them, especially anything reachable from a shared volume, init container, or plugin directory in a multi-tenant system.
  • Audit Kubernetes operators and other privileged daemons for any shell-out that trusts a tenant-writable filesystem path — this is exactly the pattern behind CVE-2026-78155.

No patch details had been published for StackGres at the time of writing; operators running it should restrict tenant database ownership privileges and monitor for anomalous escalation attempts until a fix ships.