{"id":"CVE-2026-32611","aliases":["GHSA-49g7-2ww7-3vf5","PYSEC-2026-2170"],"url":"https://o3.security/vulnerability/CVE-2026-32611","summary":"Glances has a SQL Injection in DuckDB Export via Unparameterized DDL Statements","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```","published":"2026-03-18T17:21:18.668Z","modified":"2026-08-12T03:51:12.226903965Z","cvss":{"score":7,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:L"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"glances","fixedVersion":"4.5.2"}],"fix":{"url":"https://github.com/nicolargo/glances/commit/63b7da28895249d775202d639e5531ba63491a5c","label":"nicolargo/glances@63b7da2"},"references":[{"type":"WEB","url":"https://github.com/nicolargo/glances/releases/tag/v4.5.2"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/32xxx/CVE-2026-32611.json"},{"type":"ADVISORY","url":"https://github.com/nicolargo/glances/security/advisories/GHSA-49g7-2ww7-3vf5"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-32611"},{"type":"FIX","url":"https://github.com/nicolargo/glances/commit/63b7da28895249d775202d639e5531ba63491a5c"},{"type":"PACKAGE","url":"https://github.com/nicolargo/glances"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:12.226903965Z"}}