# OliveTin has a Concurrent Template Parsing Race Condition which Leads to Cross-Request Command Contamination

OliveTin reuses one package-level text/template across goroutines; concurrent Parse/Execute races, causing cross-request command contamination and fatal concurrent-map-write crashes.

- Payout: Research
- Severity: high
- Program: OliveTin
- Found by: knight-yagami
- Disclosed: 2026-06-24
- CVE: CVE-2026-48708
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-7fq5-7wr8-rjwj
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7fq5-7wr8-rjwj
- Tags: area:web domain:web impact:dos impact:integrity kind:research platform:ghsa sev:high tech:go technique:race-window technique:source-review vuln:command-injection vuln:dos vuln:race-condition

**Target:** Go web service template engine (text/template) in action executor
**Root cause:** A single package-level *template.Template is mutated by Parse and read by Execute with no synchronization, while each ExecRequest runs in its own goroutine. Parse returns the same pointer, so concurrent actions overwrite each other's template tree.
**Steps:**
1. Identify shared `var tpl = template.New("tpl")...` in service/internal/tpl/templates.go and parseTemplate calling `tpl.Parse(source)` then `t.Execute(&sb, data)`.
2. Confirm ExecRequest spawns a goroutine per action (executor.go) reaching parseTemplate via stepParseArgs.
3. Configure two actions, e.g. `echo 'Hello {{ .Arguments.name }}'` and `rm -f /tmp/{{ .Arguments.target }}`.
4. Fire concurrent POSTs to `/api/StartAction` for both bindingIds in a loop (50+ parallel curls or the Python threading PoC).
5. Observe `fatal error: concurrent map writes` crash (check `/readyz` connection refused) or `missingkey` template errors in logs indicating contamination.
6. Optionally run `go run -race .` to confirm the data race between Parse and Execute.
**How found:** Code reading of the tpl package revealed a package-level template singleton; grep for sync/Mutex/Lock in service/internal/tpl returned nothing, and tracing ExecRequest showed per-action goroutines reaching the unsynchronized Parse+Execute, confirming the race.
**Exploitation:** An attacker able to trigger actions sends many concurrent StartAction requests for two different actions. Interleaving Parse/Execute causes one request's arguments to be rendered into another action's shell template, potentially executing unintended commands (e.g. a low-priv user's args landing in a privileged action), or crashes the process via concurrent map writes.
**Escalation:** Cross-user contamination could let a low-privilege user's arguments substitute into a higher-privilege action's command template, yielding unintended command execution; also leaks arguments (possibly secrets) into other users' output.
**Insight:** Package-level `template.New(...)` singletons are a recurring Go footgun: `Parse` mutates and returns the receiver, so any per-request Parse on a shared template races with concurrent Execute. Grep for `var .* = template.New` and check whether Parse is called per request rather than once at init.

## Proof of concept (copied exactly from the original report)

**Bash concurrent curl trigger loop**

```
#!/bin/bash
# Fire 50 concurrent requests to maximize race window
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
echo "All requests sent"
```

**Vulnerable config with two actions**

```
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
logLevel: "DEBUG"
checkForUpdates: false

actions:
  - title: Safe Echo
    id: safe-echo
    shell: "echo 'Hello {{ .Arguments.name }}'"
    arguments:
      - name: name
        type: ascii

  - title: File Delete
    id: file-delete
    shell: "rm -f /tmp/{{ .Arguments.target }}"
    arguments:
      - name: target
        type: ascii_identifier
```

