ID: 9b9a9320-d85 Priority: 89 Type: one_shot Status: open
Critical (C-1, C-2): SQL injection via f-strings in cli.py:212 (dynamic table names) and api.py:2872 (unused placeholders variable). Add table name allowlist validation in cli.py. Remove unused placeholders in api.py. Validate entity_name before queries. See security_audit_2026-04-02.md.
C-1 (cli.py): Promoted valid_tables local list to module-level _ALLOWED_DB_TABLES frozenset constant. Changed cmd_db stats to iterate over the constant and added an explicit if table not in _ALLOWED_DB_TABLES: continue validation guard as the audit recommends. Changed bare except: to except Exception:.
C-2 (api.py): Found the original unused-placeholders pattern at api.py:43113 (code heavily restructured since audit). The query at that location used SQLite ? syntax (broken in PostgreSQL), had a no-op .replace('%s', '%s' * 1), and the placeholders variable it created was never referenced in the query body. Fixed by removing placeholders, building proper per-gene LIKE clauses via gene_clauses, and using fully parameterized PostgreSQL %s bindings.
entity_name validation: Already in place at entity_detail (line 57535) and api_entity_detail (line 15289) — both validate with re.match(r"^[A-Za-z0-9_\- .,'/&()^+]+$", entity_name) and return 404 for invalid formats. No further changes needed.