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 causeThe 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 dataReproduced 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)