home / findings / wm-2026-003

Go server: unauthenticated remote code execution through shell command construction

ID
WM-2026-003
Severity
Critical  CVSS 3.1 base 9.8
Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Class
OS command injection · CWE-78 OS Command Injection
Component
media.plankdesigns.co, Go 1.22 HTTP service, POST /api/v1/thumbnail
Status
Fixed, published after the fix
Published
2026-10-04

Executive summary

The thumbnail endpoint builds a shell command line by formatting a user-supplied file name into a string and running it with sh -c. Shell metacharacters in the name are interpreted, so an unauthenticated request can run arbitrary commands as the service account.

Impact

  • Arbitrary command execution as the service user, with access to its files, environment variables and network position.
  • Compromise of secrets present in the process environment, such as database credentials and API keys.
  • A foothold inside the network for lateral movement. No authentication is required.

Technical details

root cause

The handler resizes an uploaded image by calling an external tool. The command is assembled with fmt.Sprintf and passed to a shell:

// handler.go (vulnerable)
func thumbnail(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    cmd := exec.Command("sh", "-c",
        fmt.Sprintf("convert uploads/%s -resize 200x200 thumbs/%s", name, name))
    out, err := cmd.CombinedOutput()
    if err != nil {
        http.Error(w, string(out), 500)      // also echoes command output to the caller
        return
    }
    w.WriteHeader(http.StatusNoContent)
}

Go's os/exec does not invoke a shell by itself, which is why it is normally safe. Here the code explicitly starts sh -c and hands it a string containing attacker-controlled text, so characters such as ;, $() and backticks end the intended command and start another. The error path returns the combined output, which makes results visible to the attacker.

Proof of concept

minimal, no real data

Reproduced against a local copy of the service. The command used only prints the identity of the service user.

$ curl -s "https://media.plankdesigns.co/api/v1/thumbnail?name=x.png%3Bid"
uid=1000(svc) gid=1000(svc) groups=1000(svc)

# %3B is ';' - the shell ran 'convert uploads/x.png' and then 'id'.

No payload beyond id was run, and no data was read.

Remediation

Do not use a shell. Pass the program and its arguments separately, validate the file name against a strict pattern, and stop echoing command output.

// handler.go (fixed)
var safeName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}\.(png|jpg)$`)

func thumbnail(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if !safeName.MatchString(name) {
        http.Error(w, "invalid name", http.StatusBadRequest)
        return
    }
    src := filepath.Join("uploads", name)
    dst := filepath.Join("thumbs", name)
    cmd := exec.CommandContext(r.Context(), "convert", "--", src, "-resize", "200x200", dst)
    if err := cmd.Run(); err != nil {
        log.Printf("thumbnail failed: %v", err)         // log detail, return a generic error
        http.Error(w, "processing failed", http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusNoContent)
}
  • Prefer an in-process image library over spawning a tool for untrusted input.
  • If a tool must be run, execute it in a sandbox with no network, a read-only filesystem and a syscall allow-list, and as an unprivileged user.
  • Remove secrets from the process environment where possible and use a secrets manager with short-lived credentials.
  • Add a static-analysis rule that flags exec.Command("sh", "-c", ...) and string-formatted command lines.

Detection

Alert on shell processes spawned by the service binary, and on unexpected child processes of the web service. See the Linux detections for process-creation rules.

Disclosure timeline

days from report
d0
Report sent to the vendor security contact
d1
Acknowledged
d2
Endpoint taken offline
d14
Fixed release deployed and retested
d90
Public disclosure window (published after the fix)

References

All findings

Tags

#web#go#injection#rce

Related

shares a tag