Skip to content

Insecure predictable /tmp/redis.conf enables local Redis config hijack (network exposure / root RCE primitive)

High
mperham published GHSA-j2vx-rpwf-w77v Aug 10, 2026

Package

gomod https://github.com/contribsys/faktory (Go)

Affected versions

<= 1.9.4

Patched versions

None

Description

Vulnerability Details

File: storage/redis.go (bootRedis, lines 69-106); packaging/root/usr/share/faktory/systemd/faktory.service
CWE: CWE-377 — Insecure Temporary File (related: CWE-367 — TOCTOU)
Severity: High
CVSS: 7.8 — CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Root Cause

Faktory always manages its own embedded Redis instance (there is no "point Faktory at an external Redis" option) and writes that instance'''s startup configuration to a fixed, predictable, world-writable path, /tmp/redis.conf, using a check-then-act pattern: os.Stat(conffilename) to see if it exists, and only os.WriteFile(...) if it does not. Once written, the file is never rewritten or validated on any subsequent boot.

Because /tmp is world-writable, any local unpriviliged user can create /tmp/redis.conf themselves before Faktory'''s own writer gets there -- trivially winnable on first install, first boot after a reboot on distros where /tmp is tmpfs or periodically cleared by systemd-tmpfiles-clean.timer, or any CI/CD pipeline that provisions a host and starts the service. Once won, it persists indefinitely.

Faktory'''s own CLI args only force --unixsocket, --dir, and --logfile. Every other Redis directive remains fully attacker-controlled, including bind, port, protected-mode, requirepass, rename-command, and loadmodule. The official packaging (faktory.service) runs the daemon -- and therefore the redis-server child it spawns via exec.Command, which inherits the parent'''s privileges with no drop -- as root (no User= directive), escalating this from "local unprivileged file write in /tmp" to "arbitrary configuration of, and ultimately arbitrary native code execution inside, a root-owned process".

Attack Scenario

  1. A local unprivileged user writes /tmp/redis.conf with attacker-chosen directives before Faktory'''s embedded-Redis bootstrapper ever runs.
  2. Faktory (started by systemd as root per the official unit file) boots, finds /tmp/redis.conf already present, and uses it verbatim.
  3. redis-server starts as a child of the root-owned Faktory process, applying the attacker'''s directives (e.g. bind 0.0.0.0, protected-mode no, no requirepass) while Faktory'''s own internal client keeps working normally over the CLI-forced unix socket -- the admin sees no error and no symptom.
  4. The attacker (or anyone on the network, depending on firewalling) now has full unauthenticated read/write access to 100% of the job queue contents over the network port they chose.
  5. Escalating further: adding a loadmodule /path/to/evil.so directive (Redis'''s documented module-loading feature, supported by stock redis-server builds since Redis 4.0) achieves arbitrary native code execution inside redis-server at startup -- a direct local-unprivileged-user to root escalation, given the official packaging runs as root with no privilege drop. (Not executed in this PoC to avoid loading a native module in a shared research environment; the config-hijack primitive below is sufficient to demonstrate full control.)

Impact

Silent, durable (survives restarts) full compromise of confidentiality and integrity of the entire job queue datastore via unauthenticated network exposure; a clear, documented path to local privilege escalation to root via Redis'''s native module-loading feature, given the project'''s own systemd packaging runs as root by default.

Vulnerable Code

func bootRedis(path string, sock string) (func() error, error) {
	...
	conffilename := "/tmp/redis.conf"
	if _, err := os.Stat(conffilename); err != nil {
		if os.IsNotExist(err) {
			err := os.WriteFile("/tmp/redis.conf", fmt.Appendf(nil, redisconf, client.Version), 0o444)
			...
		}
	}
	...
	cmd := exec.Command(arguments[0], arguments[1:]...) // redis-server /tmp/redis.conf --unixsocket sock --dir path --logfile logfile
	...
}
# packaging/root/usr/share/faktory/systemd/faktory.service
[Service]
ExecStart=/usr/bin/faktory -e production -b 0.0.0.0:7419 -w 0.0.0.0:7420
# no User= -> runs as root by default

Recommended Fix

conffilename := filepath.Join(path, "redis.conf") // path = operator-controlled StorageDirectory, not world-writable
if err := os.WriteFile(conffilename, fmt.Appendf(nil, redisconf, client.Version), 0o400); err != nil {
	return nil, err
}

Writing the config inside Faktory'''s own StorageDirectory instead of /tmp removes the race entirely (no other unprivileged user can pre-create or tamper with it), and always rewriting it on every boot removes the TOCTOU window altogether.

Verification

Dynamically confirmed on v1.9.4, built from the official git tag, run locally (redis-server v8.0.5, go1.26.4). Planted /tmp/redis.conf with bind 0.0.0.0, protected-mode no, no requirepass, on a custom port, before first boot. Faktory booted with zero errors and worked normally over its unix socket (PUSH/FETCH/FAIL all succeeded), while an unrelated TCP client with no credentials connected to the attacker-chosen port and ran KEYS * / CONFIG GET requirepass, reading the entire job store with no authentication. ss -ltnp confirmed genuine 0.0.0.0/[::]+ binding, not just loopback. A second variant using requirepass instead (without changing bind) caused Faktory itself to fail to boot at all ("Unable to create Faktory server: NOAUTH Authentication required."), demonstrating the same primitive can alternatively be used as a guaranteed, durable denial-of-service.

Severity

High

CVE ID

CVE-2026-63404

Weaknesses

Insecure Temporary File

Creating and using insecure temporary files can leave application and system data vulnerable to attack. Learn more on MITRE.