VYPR
Critical severity9.8NVD Advisory· Published Mar 10, 2026· Updated Apr 29, 2026

CVE-2026-30930

CVE-2026-30930

Description

Glances is an open-source system cross-platform monitoring tool. Prior to 4.5.1, 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. This vulnerability is fixed in 4.5.1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
GlancesPyPI
< 4.5.14.5.1

Affected products

1

Patches

1
39161f0d6fd7

SQL Injection via Process Names in TimescaleDB Export

https://github.com/nicolargo/glancesnicolargoMar 7, 2026via ghsa
1 file changed · +44 31
  • glances/exports/glances_timescaledb/__init__.py+44 31 modified
    @@ -10,9 +10,11 @@
     
     import sys
     import time
    +from datetime import datetime, timezone
     from platform import node
     
     import psycopg
    +from psycopg import sql
     
     from glances.exports.export import GlancesExport
     from glances.logger import logger
    @@ -77,20 +79,13 @@ def init(self):
             return db
     
         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()
    +        """Normalize the value for use in a parameterized psycopg query (returns raw Python value)."""
             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}'"
    -
    -        return f"{value}"
    +                return value[0]
    +            return ', '.join(str(v) for v in value)
    +        return value  # None → NULL, bool/str/int/float handled natively by psycopg
     
         def update(self, stats):
             """Update the TimescaleDB export module."""
    @@ -137,8 +132,8 @@ def update(self, stats):
                     segmented_by.extend(['hostname_id'])  # Segment by hostname
                     for key, value in plugin_stats.items():
                         creation_list.append(f"{key} {convert_types[type(value).__name__]} NULL")
    -                values_list.append('NOW()')  # Add the current time (insertion time)
    -                values_list.append(f"'{self.hostname}'")  # Add the hostname
    +                values_list.append(datetime.now(timezone.utc))  # Add the current time (insertion time)
    +                values_list.append(self.hostname)  # Add the hostname
                     values_list.extend([self.normalize(value) for value in plugin_stats.values()])
                     values_list = [values_list]
                 elif isinstance(plugin_stats, list) and len(plugin_stats) > 0 and 'key' in plugin_stats[0]:
    @@ -153,9 +148,9 @@ def update(self, stats):
                     # Create the values list (it is a list of list to have a single datamodel for all the plugins)
                     for plugin_item in plugin_stats:
                         item_list = []
    -                    item_list.append('NOW()')  # Add the current time (insertion time)
    -                    item_list.append(f"'{self.hostname}'")  # Add the hostname
    -                    item_list.append(f"'{plugin_item.get('key')}'")
    +                    item_list.append(datetime.now(timezone.utc))  # Add the current time (insertion time)
    +                    item_list.append(self.hostname)  # Add the hostname
    +                    item_list.append(plugin_item.get('key'))
                         item_list.extend([self.normalize(value) for value in plugin_item.values()])
                         values_list.append(item_list[:-1])
                 else:
    @@ -175,34 +170,52 @@ def export(self, plugin, creation_list, segmented_by, values_list):
     
             with self.client.cursor() as cur:
                 # Is the table exists?
    -            cur.execute(f"select exists(select * from information_schema.tables where table_name='{plugin}')")
    +            cur.execute(
    +                "SELECT EXISTS(SELECT * FROM information_schema.tables WHERE table_name=%s)",
    +                [plugin],
    +            )
                 if not cur.fetchone()[0]:
                     # Create the table if it does not exist
                     # https://github.com/timescale/timescaledb/blob/main/README.md#create-a-hypertable
    -                # Execute the create table query
    -                create_query = f"""
    -CREATE TABLE {plugin} (
    -    {', '.join(creation_list)}
    -)
    -WITH (
    -    timescaledb.hypertable,
    -    timescaledb.partition_column='time',
    -    timescaledb.segmentby = '{", ".join(segmented_by)}'
    -);"""
    +                # Build CREATE TABLE using sql.Identifier for column names (prevents injection)
    +                # Each item in creation_list is "colname TYPE [NULL|NOT NULL]"
    +                fields = sql.SQL(', ').join(
    +                    sql.SQL("{} {}").format(
    +                        sql.Identifier(item.split(' ')[0]),
    +                        sql.SQL(' '.join(item.split(' ')[1:]))
    +                    )
    +                    for item in creation_list
    +                )
    +                create_query = sql.SQL(
    +                    "CREATE TABLE {table} ({fields}) WITH ("
    +                    "timescaledb.hypertable, "
    +                    "timescaledb.partition_column='time', "
    +                    "timescaledb.segmentby = {segmentby});"
    +                ).format(
    +                    table=sql.Identifier(plugin),
    +                    fields=fields,
    +                    segmentby=sql.Literal(', '.join(segmented_by)),
    +                )
                     logger.debug(f"Create table: {create_query}")
                     try:
                         cur.execute(create_query)
                     except Exception as e:
                         logger.error(f"Cannot create table {plugin}: {e}")
                         return
     
    -            # Insert the data
    +            # Insert the data using parameterized queries (prevents injection)
                 # https://github.com/timescale/timescaledb/blob/main/README.md#insert-and-query-data
    -            insert_list = [f"({','.join(i)})" for i in values_list]
    -            insert_query = f"INSERT INTO {plugin} VALUES {','.join(insert_list)};"
    +            col_names = [item.split(' ')[0] for item in creation_list]
    +            cols = sql.SQL(', ').join(sql.Identifier(c) for c in col_names)
    +            placeholders = sql.SQL(', ').join(sql.Placeholder() for _ in col_names)
    +            insert_query = sql.SQL("INSERT INTO {table} ({cols}) VALUES ({vals})").format(
    +                table=sql.Identifier(plugin),
    +                cols=cols,
    +                vals=placeholders,
    +            )
                 logger.debug(f"Insert data into table: {insert_query}")
                 try:
    -                cur.execute(insert_query)
    +                cur.executemany(insert_query, values_list)
                 except Exception as e:
                     logger.error(f"Cannot insert data into table {plugin}: {e}")
                     return
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.