GitHub Security Advisory: GHSA-vx5f-957p-qpvm
Package: Glances (PIP)
Severity: HIGH (CVSS 8.1)
CWE: CWE-346: Origin Validation Error, CWE-522: Insufficiently Protected Credentials
Vulnerable versions: < 4.5.2
Patched in: 4.5.2

Summary: Glances Central Browser Autodiscovery Leaks Reusable Credentials to Zeroconf-Spoofed Servers

Details:
## Summary

In Central Browser mode, Glances stores both the Zeroconf-advertised server name and the discovered IP address for dynamic servers, but later builds connection URIs from the untrusted advertised name instead of the discovered IP. When a dynamic server reports itself as protected, Glances also uses that same untrusted name as the lookup key for saved passwords and the global `[passwords] default` credential.

An attacker on the same local network can advertise a fake Glances service over Zeroconf and cause the browser to automatically send a reusable Glances authentication secret to an attacker-controlled host. This affects the background polling path and the REST/WebUI click-through path in Central Browser mode.

## Details

Dynamic server discovery keeps both a short `name` and a separate `ip`:

```python
# glances/servers_list_dynamic.py:56-61
def add_server(self, name, ip, port, protocol='rpc'):
    new_server = {
        'key': name,
        'name': name.split(':')[0],  # Short name
        'ip': ip,  # IP address seen by the client
        'port': port,
        ...
        'type': 'DYNAMIC',
    }
```

The Zeroconf listener populates those fields directly from the service advertisement:

```python
# glances/servers_list_dynamic.py:112-121
new_server_ip = socket.inet_ntoa(address)
new_server_port = info.port
...
self.servers.add_server(
    srv_name,
    new_server_ip,
    new_server_port,
    protocol=new_server_protocol,
)
```

However, the Central Browser connection logic ignores `server['ip']` and instead uses the untrusted advertised `server['name']` for both password lookup and the destination 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
```

That URI is used automatically by the background polling thread:

```python
# glances/servers_list.py:141-143
def __update_stats(self, server):
    server['uri'] = self.get_uri(server)
```

The password lookup itself falls back to the global default password when there is no exact match:

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

The sample configuration explicitly supports that `default` credential reuse:

```ini
# conf/glances.conf:656-663
[passwords]
# Define the passwords li