{"repo":"NousResearch/hermes-agent","public_id":"1911","display_label":"SQL injection risk and multi-platform bug fixes","summary":"These items address a potential SQL injection vulnerability in execute() calls via string formatting, along with a broader fix touching Discord, Slack, WhatsApp, and rate limiting concerns. The SQL security issue is the clearest shared theme, with the third item bundling several unrelated platform fixes.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":2061,"merged_at":"2026-03-19T16:52:09+00:00"},"resolution":{"state":"resolved","by":"pr-2061"},"freshness":{"state":"stale","last_verified_at":"2026-03-19T16:52:09+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":1911,"pr_number":2061,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-19T16:52:09+00:00"}],"members":[{"kind":"issue","number":1911,"title":"[Security] possible SQL injection via string formatting in execute()","url":null,"state":"closed"},{"kind":"pr","number":2032,"title":"Fix 5 high-impact issues: SQL security, Discord threads, Slack context, WhatsApp delivery, rate limit UX","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#1911","number":1911},{"kind":"pr","value":"#2011","number":2011},{"kind":"pr","value":"#1826","number":1826},{"kind":"pr","value":"#2724","number":2724},{"kind":"pr","value":"#1953","number":1953},{"kind":"pr","value":"#1945","number":1945}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":2061,"title":"fix(security): eliminate SQL string formatting in execute() calls","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/insights.py","hermes_state.py","tests/test_sql_injection.py"],"fix_hunks":[{"file":"agent/insights.py","hunks":["@@ -181,22 +181,25 @@ def generate(self, days: int = 30, source: str = None) -> Dict[str, Any]:\n                      \"billing_base_url, billing_mode, estimated_cost_usd, \"\n                      \"actual_cost_usd, cost_status, cost_source\")\n \n+    # Pre-computed query strings — f-string evaluated once at class definition,\n+    # not at runtime, so no user-controlled value can alter the query structure.\n+    _GET_SESSIONS_WITH_SOURCE = (\n+        f\"SELECT {_SESSION_COLS} FROM sessions\"\n+        \" WHERE started_at >= ? AND source = ?\"\n+        \" ORDER BY started_at DESC\"\n+    )\n+    _GET_SESSIONS_ALL = (\n+        f\"SELECT {_SESSION_COLS} FROM sessions\"\n+        \" WHERE started_at >= ?\"\n+        \" ORDER BY started_at DESC\"\n+    )\n+\n     def _get_sessions(self, cutoff: float, source: str = None) -> List[Dict]:\n         \"\"\"Fetch sessions within the time window.\"\"\"\n         if source:\n-            cursor = self._conn.execute(\n-                f\"\"\"SELECT {self._SESSION_COLS} FROM sessions\n-                    WHERE started_at >= ? AND source = ?\n-                    ORDER BY started_at DESC\"\"\",\n-                (cutoff, source),\n-            )\n+            cursor = self._conn.execute(self._GET_SESSIONS_WITH_SOURCE, (cutoff, source))\n         else:\n-            cursor = self._conn.execute(\n-                f\"\"\"SELECT {self._SESSION_COLS} FROM sessions\n-                    WHERE started_at >= ?\n-                    ORDER BY started_at DESC\"\"\",\n-                (cutoff,),\n-            )\n+            cursor = self._conn.execute(self._GET_SESSIONS_ALL, (cutoff,))\n         return [dict(row) for row in cursor.fetchall()]\n \n     def _get_tool_usage(self, cutoff: float, source: str = None) -> List[Dict]:"]},{"file":"hermes_state.py","hunks":["@@ -181,7 +181,11 @@ def _init_schema(self):\n                 ]\n                 for name, column_type in new_columns:\n                     try:\n-                        cursor.execute(f\"ALTER TABLE sessions ADD COLUMN {name} {column_type}\")\n+                        # name and column_type come from the hardcoded tuple above,\n+                        # not user input. Double-quote identifier escaping is applied\n+                        # as defense-in-depth; SQLite DDL cannot be parameterized.\n+                        safe_name = name.replace('\"', '\"\"')\n+                        cursor.execute(f'ALTER TABLE sessions ADD COLUMN \"{safe_name}\" {column_type}')\n                     except sqlite3.OperationalError:\n                         pass\n                 cursor.execute(\"UPDATE schema_version SET version = 5\")"]},{"file":"tests/test_sql_injection.py","hunks":["@@ -0,0 +1,43 @@\n+\"\"\"Tests that verify SQL injection mitigations in insights and state modules.\"\"\"\n+\n+import re\n+\n+from agent.insights import InsightsEngine\n+\n+\n+def test_session_cols_no_injection_chars():\n+    \"\"\"_SESSION_COLS must not contain SQL injection vectors.\"\"\"\n+    cols = InsightsEngine._SESSION_COLS\n+    assert \";\" not in cols\n+    assert \"--\" not in cols\n+    assert \"'\" not in cols\n+    assert \"DROP\" not in cols.upper()\n+\n+\n+def test_get_sessions_all_query_is_parameterized():\n+    \"\"\"_GET_SESSIONS_ALL must use a ? placeholder for the cutoff value.\"\"\"\n+    query = InsightsEngine._GET_SESSIONS_ALL\n+    assert \"?\" in query\n+    assert \"started_at >= ?\" in query\n+    # Must not embed any runtime-variable content via brace interpolation\n+    assert \"{\" not in query\n+\n+\n+def test_get_sessions_with_source_query_is_parameterized():\n+    \"\"\"_GET_SESSIONS_WITH_SOURCE must use ? placeholders for both parameters.\"\"\"\n+    query = InsightsEngine._GET_SESSIONS_WITH_SOURCE\n+    assert query.count(\"?\") == 2\n+    assert \"started_at >= ?\" in query\n+    assert \"source = ?\" in query\n+    assert \"{\" not in query\n+\n+\n+def test_session_col_names_are_safe_identifiers():\n+    \"\"\"Every column name listed in _SESSION_COLS must be a simple identifier.\"\"\"\n+    cols = InsightsEngine._SESSION_COLS\n+    identifiers = [c.strip() for c in cols.split(\",\")]\n+    safe_identifier = re.compile(r\"^[a-zA-Z_][a-zA-Z0-9_]*$\")\n+    for col in identifiers:\n+        assert safe_identifier.match(col), (\n+            f\"Column name {col!r} is not a safe SQL identifier\"\n+        )\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}