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:
# 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.
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: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 thekeyagainst an allowed-keys list; thevalueis never sanitized. The table/library name is guarded bysafe_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), andQuery.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.