Vulnerability Details
File: server/commands.go
Functions/Lines: queue() lines 51,69 — pushBulk() line 167 — push() line 212 — ack() line 268 — fail() line 292 — heartbeat() (BEAT) line 333
CWE: CWE-248 — Uncaught Exception (primary), CWE-617, CWE-400 (impact)
Severity: Critical
CVSS: 8.6 — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (unauthenticated / no password configured); 7.1 with PR:L if a password is configured (any already-connected producer/worker still suffices)
Root Cause
Faktory'''s wire protocol is line-based: the server reads one line, looks up the verb in CommandSet, and calls the handler with the raw unparsed line. Several handlers slice the line at a fixed byte offset (e.g. cmd[5:]) or index into a fixed slice position (qs[0], qs[1]) WITHOUT checking that the line actually contains a payload. Sending the bare verb alone (no payload) causes a Go runtime panic (slice/index out of range).
Critically, there is no recover() anywhere in the entire codebase (verified via grep -rn "recover()" across all non-test source — zero results). An unrecovered panic in any goroutine terminates the ENTIRE Go process, not just that connection'''s goroutine. So a single malformed one-line command from any client crashes the whole server -- every other connection, every in-flight job, every worker, instantly disconnected.
This requires no special privilege beyond what any ordinary producer/consumer already has (or zero privilege at all if the operator hasn'''t configured a password, which is the project'''s own "development" environment default).
Attack Scenario
- Attacker connects to the Faktory command port (default 7419) and completes the trivial HELLO handshake (no credentials needed if unconfigured; the standard shared password otherwise).
- Attacker sends a single line containing ONLY a bare verb, e.g.
PUSH , ACK , FAIL , BEAT , PUSHB , or QUEUE .
- The handler panics on the unguarded slice/index operation; nothing recovers it; the entire Faktory process exits.
- Every other client/worker on the instance is disconnected immediately; the whole job-processing pipeline is down until something restarts the process. The attacker can repeat this indefinitely to keep the service down.
Impact
Complete, 100%-reproducible, on-demand Denial of Service of the entire Faktory instance (not just the attacker'''s own connection) via a single short line of text, requiring no special privilege.
Vulnerable Code
func queue(c *Connection, s *Server, cmd string) {
qs := strings.Split(cmd, " ")[1:]
subcmd := strings.ToUpper(qs[0]) // panics if cmd == "QUEUE" (qs is empty)
...
if op != nil {
if qs[1] == "*" { // panics if cmd == "QUEUE PAUSE" (qs has only 1 element)
func push(c *Connection, s *Server, cmd string) {
data := cmd[5:] // panics if cmd == "PUSH" (len 4 < 5)
func ack(c *Connection, s *Server, cmd string) {
data := cmd[4:] // panics if cmd == "ACK" (len 3 < 4)
func fail(c *Connection, s *Server, cmd string) {
data := cmd[5:] // panics if cmd == "FAIL" (len 4 < 5)
func heartbeat(c *Connection, s *Server, cmd string) {
data := cmd[5:] // panics if cmd == "BEAT" (len 4 < 5)
func pushBulk(c *Connection, s *Server, cmd string) {
data := cmd[6:] // panics if cmd == "PUSHB" (len 5 < 6)
The dispatch loop in server/server.go (processLines) calls proc(conn, s, cmd) directly with no recover() wrapper anywhere.
Recommended Fix
- Add a
recover() wrapper around command dispatch in processLines so a handler bug degrades to one client error/disconnect, not a full server crash:
func safeDispatch(proc command, conn *Connection, s *Server, cmd string) {
defer func() {
if r := recover(); r != nil {
util.Error("panic handling command", fmt.Errorf("%v", r))
_ = conn.Error(cmd, fmt.Errorf("internal error"))
}
}()
proc(conn, s, cmd)
}
- Validate command shape before slicing/indexing in each handler, e.g.:
func push(c *Connection, s *Server, cmd string) {
if len(cmd) < 6 || cmd[4] != ''' ''' {
_ = c.Error(cmd, fmt.Errorf("invalid PUSH, missing payload"))
return
}
data := cmd[5:]
...
}
The same len(cmd) < N guard applies to pushBulk, ack, fail, heartbeat, and a len(qs) < N guard to queue.
Verification
Dynamically confirmed on v1.9.4, built from the official git tag, run locally (go1.26.4, embedded Redis v8.0.5). Six independent bare-verb commands (QUEUE, ACK, FAIL, PUSH, BEAT, PUSHB) were each sent on a freshly restarted instance; every single one produced a full process crash, confirmed via ps aux (process gone) and a captured Go panic stack trace pinpointing the exact unguarded slice/index operation in server/commands.go, propagating uncaught through server.(*Server).processLines with no recovery anywhere in the call chain.
Vulnerability Details
File:
server/commands.goFunctions/Lines:
queue()lines 51,69 —pushBulk()line 167 —push()line 212 —ack()line 268 —fail()line 292 —heartbeat()(BEAT) line 333CWE: CWE-248 — Uncaught Exception (primary), CWE-617, CWE-400 (impact)
Severity: Critical
CVSS: 8.6 — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (unauthenticated / no password configured); 7.1 with PR:L if a password is configured (any already-connected producer/worker still suffices)
Root Cause
Faktory'''s wire protocol is line-based: the server reads one line, looks up the verb in
CommandSet, and calls the handler with the raw unparsed line. Several handlers slice the line at a fixed byte offset (e.g.cmd[5:]) or index into a fixed slice position (qs[0],qs[1]) WITHOUT checking that the line actually contains a payload. Sending the bare verb alone (no payload) causes a Go runtime panic (slice/index out of range).Critically, there is no
recover()anywhere in the entire codebase (verified viagrep -rn "recover()"across all non-test source — zero results). An unrecovered panic in any goroutine terminates the ENTIRE Go process, not just that connection'''s goroutine. So a single malformed one-line command from any client crashes the whole server -- every other connection, every in-flight job, every worker, instantly disconnected.This requires no special privilege beyond what any ordinary producer/consumer already has (or zero privilege at all if the operator hasn'''t configured a password, which is the project'''s own "development" environment default).
Attack Scenario
PUSH,ACK,FAIL,BEAT,PUSHB, orQUEUE.Impact
Complete, 100%-reproducible, on-demand Denial of Service of the entire Faktory instance (not just the attacker'''s own connection) via a single short line of text, requiring no special privilege.
Vulnerable Code
The dispatch loop in
server/server.go(processLines) callsproc(conn, s, cmd)directly with no recover() wrapper anywhere.Recommended Fix
recover()wrapper around command dispatch inprocessLinesso a handler bug degrades to one client error/disconnect, not a full server crash:The same
len(cmd) < Nguard applies topushBulk,ack,fail,heartbeat, and alen(qs) < Nguard toqueue.Verification
Dynamically confirmed on v1.9.4, built from the official git tag, run locally (go1.26.4, embedded Redis v8.0.5). Six independent bare-verb commands (QUEUE, ACK, FAIL, PUSH, BEAT, PUSHB) were each sent on a freshly restarted instance; every single one produced a full process crash, confirmed via
ps aux(process gone) and a captured Go panic stack trace pinpointing the exact unguarded slice/index operation in server/commands.go, propagating uncaught through server.(*Server).processLines with no recovery anywhere in the call chain.