SQL injection via unescaped metadata/filter value in the collection-database layer (cross-document disclosure)

#1304 · open · 0 comments

View on GitHub ↗

geo-chen

### Summary llmware's SQLite and PostgreSQL collection-database backends build WHERE clauses by string-interpolating filter and lookup values directly into SQL, with no parameterization or escaping. A filter value containing SQL metacharacters injects into the query, neutralizing the intended filter and returning rows the caller was scoped out of (cross-document / cross-collection disclosure), and on PostgreSQL enabling full boolean/UNION-based injection. Confirmed against the real `SQLiteRetrieval.filter_by_key_dict`: a benign filter returned one row, while an injected value returned a different, secret document. ### Details In `llmware/resources.py`, the SQLite retrieval methods concatenate the value into the SQL string: ```python # SQLiteRetrieval.filter_by_key_dict (~line 2339) conditions_clause += f" {key} = '{value}' AND " # SQLiteRetrieval.text_search_with_key_value_dict_filter (~line 2280) ... f" AND {key} = '{value}'" ``` The PostgreSQL backend has the identical pattern (`resources.py` ~lines 1200 and 1239), with no FTS5 MATCH restriction, so tautology and UNION payloads are unrestricted there. The filter validator (`llmware/retrieval.py` ~lines 549 to 553) checks only the `key` against an allowed-keys list; the `value` is never sanitized. The table/library name is guarded by `safe_name`, but values are not. These value sinks are reached from the public API: `Library.block_lookup(block_id, doc_id)` -> `filter_by_key_dict` (`library.py` ~line 1265), and `Query.text_query_with_custom_filter` / `text_query_by_author_or_speaker` -> `text_search_with_key_value_dict_filter` (`retrieval.py` ~lines 532 and 540). In a RAG or multi-tenant deployment these values derive from end-user query input or from document/metadata identifiers. Happy to share a poc.

Comments