GitHub Security Advisory: GHSA-r297-p3v4-wp8m
Package: Glances (PIP)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-200: Exposure of Sensitive Information to an Unauthorized Actor, CWE-522: Insufficiently Protected Credentials
Vulnerable versions: <= 4.5.2-dev01
Patched in: 4.5.2

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

Details:
## 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.[CREDENTIAL_REDACTED] = 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[ho