GHSA-qcpp-8x79-hhp3 Glances has a command injection bypass of action-template sanitizer via cross-field shell-operator reconstruction
SummaryThe 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` `<= 4.5.5` (verified against the published PyPI release `4.5.5`, the
latest at the time of writing; `glances.__version__ == "4.5.5"`). The
per-field sanitizer `_sanitize_mustache_dict()` is present and active in this
release. Not patched in any released version.Privilege requiredTwo roles are involved:A local, unprivileged user (or a container the attacker can name) supplies the
attacker-controlled stat values (process/container name, mount point, etc.).
This is the same trust boundary the action-template command-injection class
already recognises: the attacker controls the process name, not the
configuration.An administrator has configured an action whose command template concatenates
two **unescaped** Mustache variables with no separating character
(`{{{name}}}{{{cmdline}}}`). Unescaped Mustache (`{{{ }}}` / `{{& }}`) is a
documented Chevron feature and is the natural choice when the operator wants a
value that contains shell-significant characters to reach the command verbatim.No network access to the target host is required beyond the ability to run a
process (or start a named container) on it.Vulnerable code (file:line)`glances/actions.py:25-46` — the per-field sanitizer:`glances/actions.py:100-111` — sanitize-then-render-then-execute: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 sinkA local unprivileged user starts a process (or a container) whose `name`
ends with `&` and whose `cmdline` begins with `& <command>` (both are stored
verbatim in the plugin stat item).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.`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.`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`)`repro.py`:Captured output (`glances` 4.5.5, Python 3.13, x86_64 Linux):The negative control confirms that the same attacker values under a standard
double-brace template are blocked (chevron escapes `&`). The positive case shows
the injected `touch` executing when the template uses two adjacent unescaped
variables: the file `/tmp/glances_crossfield_pwned` is created by the injected
command, not by the intended `logger` action.ImpactArbitrary command execution as the OS user running Glances (frequently root on
monitored hosts) whenever an operator uses an unescaped, adjacent-variable
action template and an attacker controls two neighbouring stat fields.The same reconstruction reaches `secure_popen()`'s file-redirection (`>`) and
pipe (`|`) handling, allowing arbitrary file write and output piping in
addition to command chaining.The bypass defeats the dedicated `_sanitize_mustache_dict()` control that was
added specifically to stop attacker-controlled stat values from injecting shell
operators.Suggested fixEnforce the operator ban on the **rendered** command string that comes from
template-variable expansion, rather than on the pre-render values in isolation.
One approach that mirrors the existing helper: render each variable, then reject /
neutralise operators in the concatenated result, or strip lone `&`/redirection
characters that originate from variable data.Neutralising the single `&` (and the single `>` / `|`) in each value removes the
cross-field reconstruction because no operator character survives on either side
of a variable boundary. Alternatively, sanitize `cmd_full` after
`chevron.render()`, or pass the template-derived data as `secure_popen(...,
allow_operators=False)` when the command originates from stat-field substitution.CreditReported by tonghuaroot.
CVSS v4.0 score: 8.8