VYPR
High severity8.3NVD Advisory· Published May 29, 2026· Updated May 29, 2026

CVE-2026-10105

CVE-2026-10105

Description

agno 2.6.5 contains a SQL injection vulnerability in the ClickHouse vector database backend that allows attackers to inject arbitrary SQL expressions by supplying malicious metadata keys and values to the delete_by_metadata() method. Attackers can exploit the unsafe f-string interpolation in clickhousedb.py to delete all rows, target specific rows, or extract information through error-based or blind SQL injection techniques.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

SQL injection in agno 2.6.5 ClickHouse vector database via unsafe f-string interpolation in delete_by_metadata allows arbitrary data deletion or extraction.

Vulnerability

A SQL injection vulnerability exists in agno version 2.6.5 in the ClickHouse vector database backend, specifically in the delete_by_metadata() method of clickhousedb.py. User-controlled metadata keys and values are directly interpolated into a ClickHouse SQL DELETE statement via unsafe f-string interpolation (f"JSONExtractString(toString(filters), '{key}') = '{value}'"). The method builds WHERE conditions without any escaping or parameterization of the keys or values, while table and database names are safely handled with {...:Identifier} parameters. This affects users who call delete_by_metadata() with metadata sourced from untrusted input [1].

Exploitation

An attacker with network access to the application that uses agno's ClickHouse vector database can supply malicious metadata keys and/or values to the delete_by_metadata() method. The attacker need only control the dictionary passed to this method, enabling injection of arbitrary ClickHouse SQL expressions. For example, a value like ' OR 1=1 -- can delete all rows, or an error-based injection can extract data through boolean or time-based blind techniques. No authentication beyond the application's normal access is required, and no user interaction is needed apart from triggering the vulnerable code path [1].

Impact

Successful exploitation allows an attacker to delete arbitrary rows from any table accessible by the ClickHouse connection, target specific rows for deletion, or extract sensitive information through error-based or blind SQL injection. The attacker can achieve unauthorized data modification (integrity breach) and information disclosure (confidentiality breach) at the privilege level of the ClickHouse database client used by agno [1].

Mitigation

A fix was merged in pull request #7883 on May 12, 2026 [2][3][4]. The patch replaces direct f-string interpolation with ClickHouse named parameters ({meta_key_N:String} and {meta_val_N:String|Float64|Bool}) for both keys and values, eliminating the injection vector. Users should upgrade to the patched version of agno as soon as it is released. No workaround is provided for version 2.6.5, and the vulnerability is not listed in CISA's Known Exploited Vulnerabilities (KEV) catalog as of publication [2][3].

AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Agno/Agnoreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: 2.6.5

Patches

1
3aa5036384f7

Merge a0ec99305e782e68ba26f5966c53ad50b5f40132 into 93052298f29334ac91924495dcba2d94394f700e

https://github.com/agno-agi/agnoPRABHU KIRAN VANDRANKIMay 12, 2026via nvd-ref
2 files changed · +149 5
  • libs/agno/agno/vectordb/clickhouse/clickhousedb.py+21 5 modified
    @@ -683,15 +683,31 @@ def delete_by_metadata(self, metadata: Dict[str, Any]) -> bool:
                 log_debug(f"ClickHouse VectorDB : Deleting documents with metadata {metadata}")
                 parameters = self._get_base_parameters()
     
    -            # Build WHERE clause for metadata matching using proper ClickHouse JSON syntax
    +            # Build a parameterised WHERE clause so that user-supplied metadata
    +            # keys and values are never interpolated directly into SQL.
    +            # Each key/value pair gets its own named ClickHouse parameter
    +            # ({meta_key_N:String} / {meta_val_N:String|Float64|Bool}) so the
    +            # client driver handles escaping, eliminating the SQL-injection path.
                 where_conditions = []
    -            for key, value in metadata.items():
    +            for i, (key, value) in enumerate(metadata.items()):
    +                param_key = f"meta_key_{i}"
    +                param_val = f"meta_val_{i}"
    +                parameters[param_key] = key
                     if isinstance(value, bool):
    -                    where_conditions.append(f"JSONExtractBool(toString(filters), '{key}') = {str(value).lower()}")
    +                    parameters[param_val] = value
    +                    where_conditions.append(
    +                        f"JSONExtractBool(toString(filters), {{{param_key}:String}}) = {{{param_val}:Bool}}"
    +                    )
                     elif isinstance(value, (int, float)):
    -                    where_conditions.append(f"JSONExtractFloat(toString(filters), '{key}') = {value}")
    +                    parameters[param_val] = float(value)
    +                    where_conditions.append(
    +                        f"JSONExtractFloat(toString(filters), {{{param_key}:String}}) = {{{param_val}:Float64}}"
    +                    )
                     else:
    -                    where_conditions.append(f"JSONExtractString(toString(filters), '{key}') = '{value}'")
    +                    parameters[param_val] = str(value)
    +                    where_conditions.append(
    +                        f"JSONExtractString(toString(filters), {{{param_key}:String}}) = {{{param_val}:String}}"
    +                    )
     
                 if not where_conditions:
                     return False
    
  • libs/agno/tests/unit/vectordb/test_clickhouse_sql_injection.py+128 0 added
    @@ -0,0 +1,128 @@
    +"""
    +Unit tests for ClickHouseVectorDb.delete_by_metadata SQL injection fix.
    +
    +Tests verify that user-controlled metadata keys/values are passed as
    +ClickHouse named parameters, not interpolated directly into SQL strings.
    +
    +Fixes: https://github.com/agno-agi/agno/issues/7866
    +"""
    +from unittest.mock import MagicMock, patch
    +
    +
    +def _make_db():
    +    """Build a ClickHouseVectorDb instance with a mocked client."""
    +    with patch("clickhouse_connect.get_client"):
    +        from agno.vectordb.clickhouse.clickhousedb import ClickHouseDb
    +
    +        db = ClickHouseDb(
    +            table="test_table",
    +            host="localhost",
    +            database="test_db",
    +        )
    +    db.client = MagicMock()
    +    db.client.command.return_value = None
    +    return db
    +
    +
    +class TestDeleteByMetadataSqlInjection:
    +    """delete_by_metadata must use parameterised queries, not f-string SQL."""
    +
    +    def test_string_value_uses_parameter_not_interpolation(self):
    +        """String values must appear in the parameters dict, not in the SQL."""
    +        db = _make_db()
    +        injection = "'; DROP TABLE test_table; --"
    +        db.delete_by_metadata({"category": injection})
    +
    +        db.client.command.assert_called_once()
    +        call_kwargs = db.client.command.call_args
    +
    +        query = call_kwargs[0][0] if call_kwargs[0] else call_kwargs[1]["query"]
    +        params = call_kwargs[1].get("parameters", {}) or (
    +            call_kwargs[0][1] if len(call_kwargs[0]) > 1 else {}
    +        )
    +
    +        # The injection string must NOT appear anywhere in the SQL text
    +        assert injection not in query, (
    +            f"Injection string leaked into SQL: {query!r}"
    +        )
    +        # It must be in the parameters dict instead
    +        assert injection in params.values(), (
    +            f"Injection string not found in parameters: {params}"
    +        )
    +
    +    def test_key_uses_parameter_not_interpolation(self):
    +        """Metadata keys must also be parameterised — they are user-controlled."""
    +        db = _make_db()
    +        malicious_key = "x') = 1 OR (JSONExtractString(toString(filters), 'y"
    +        db.delete_by_metadata({malicious_key: "safe_value"})
    +
    +        db.client.command.assert_called_once()
    +        call_kwargs = db.client.command.call_args
    +        query = call_kwargs[0][0] if call_kwargs[0] else call_kwargs[1]["query"]
    +        params = call_kwargs[1].get("parameters", {}) or (
    +            call_kwargs[0][1] if len(call_kwargs[0]) > 1 else {}
    +        )
    +
    +        assert malicious_key not in query, (
    +            f"Malicious key leaked into SQL: {query!r}"
    +        )
    +        assert malicious_key in params.values(), (
    +            f"Malicious key not found in parameters: {params}"
    +        )
    +
    +    def test_numeric_value_uses_parameter(self):
    +        """Numeric values are passed as Float64 parameters."""
    +        db = _make_db()
    +        db.delete_by_metadata({"score": 3.14})
    +
    +        call_kwargs = db.client.command.call_args
    +        query = call_kwargs[0][0] if call_kwargs[0] else call_kwargs[1]["query"]
    +        params = call_kwargs[1].get("parameters", {}) or (
    +            call_kwargs[0][1] if len(call_kwargs[0]) > 1 else {}
    +        )
    +
    +        assert "3.14" not in query
    +        assert 3.14 in params.values()
    +
    +    def test_bool_value_uses_parameter(self):
    +        """Boolean values are passed as Bool parameters."""
    +        db = _make_db()
    +        db.delete_by_metadata({"active": True})
    +
    +        call_kwargs = db.client.command.call_args
    +        query = call_kwargs[0][0] if call_kwargs[0] else call_kwargs[1]["query"]
    +        params = call_kwargs[1].get("parameters", {}) or (
    +            call_kwargs[0][1] if len(call_kwargs[0]) > 1 else {}
    +        )
    +
    +        # "true" / "True" must not be raw-interpolated
    +        assert "= true" not in query.lower() or "{" in query  # placeholder is in the SQL
    +        assert True in params.values()
    +
    +    def test_multiple_conditions_all_parameterised(self):
    +        """All conditions in a multi-key dict use separate named parameters."""
    +        db = _make_db()
    +        db.delete_by_metadata({"env": "prod", "region": "us-east-1"})
    +
    +        call_kwargs = db.client.command.call_args
    +        query = call_kwargs[0][0] if call_kwargs[0] else call_kwargs[1]["query"]
    +        params = call_kwargs[1].get("parameters", {}) or (
    +            call_kwargs[0][1] if len(call_kwargs[0]) > 1 else {}
    +        )
    +
    +        # Raw strings must NOT appear in SQL
    +        assert "prod" not in query
    +        assert "us-east-1" not in query
    +        assert "env" not in query
    +        assert "region" not in query
    +
    +        # Both values must be in parameters
    +        assert "prod" in params.values()
    +        assert "us-east-1" in params.values()
    +
    +    def test_empty_metadata_returns_false(self):
    +        """Empty metadata dict returns False without calling client.command."""
    +        db = _make_db()
    +        result = db.delete_by_metadata({})
    +        assert result is False
    +        db.client.command.assert_not_called()
    

Vulnerability mechanics

Root cause

"Unsafe f-string interpolation of user-controlled metadata keys and values into ClickHouse SQL DELETE statements in delete_by_metadata()."

Attack vector

An attacker with the ability to call `delete_by_metadata()` (which requires low-level access to the vector database API) can supply arbitrary metadata keys and values. Because the method interpolated these directly into the SQL WHERE clause via f-strings, a payload such as `{'source': "' OR '1'='1"}` produces `WHERE ... = '' OR '1'='1'`, a tautology that deletes all rows. Both key and value injection are possible, enabling mass deletion, targeted deletion, or data extraction through error-based or blind SQL injection techniques [ref_id=1].

Affected code

The vulnerability resides in `agno/vectordb/clickhouse/clickhousedb.py` in the `delete_by_metadata()` method. User-controlled metadata keys and values were directly interpolated into ClickHouse SQL DELETE statements via f-string, without parameterization or escaping. The patch modifies this method to use ClickHouse named parameters (`{meta_key_N:String}`, `{meta_val_N:String|Float64|Bool}`) instead of raw interpolation, and adds unit tests in `libs/agno/tests/unit/vectordb/test_clickhouse_sql_injection.py` to verify the fix.

What the fix does

The patch replaces direct f-string interpolation with ClickHouse's native `{name:Type}` parameter syntax. Each metadata key and value is assigned a unique named parameter (`meta_key_i`, `meta_val_i`) and added to the `parameters` dict with an appropriate ClickHouse type (`String`, `Float64`, or `Bool`). The ClickHouse client driver then handles escaping, eliminating the SQL injection path. Unit tests confirm that injection strings appear only in the parameters dict, never in the SQL text itself [patch_id=3104653][ref_id=2].

Preconditions

  • authAttacker must be able to call delete_by_metadata() on a ClickHouse vector database instance
  • inputAttacker controls the metadata dictionary keys and/or values passed to the method

Generated on May 29, 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.