fix(memory): escape scope and id values in LanceDB filters - #7820
stepchanges wants to merge 2 commits into
Conversation
LanceDBStorage built its where() filters by interpolating scope prefixes and record ids into SQL strings. Route every interpolated value through a quote-escaping helper, and match scope prefixes with LIKE ... ESCAPE '\' so %, _ and \ in scope names match literally while the trailing prefix wildcard is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughLanceDB storage now escapes record IDs and scope prefixes when it builds SQL filters. Integration tests check literal matching and tenant isolation across searches, listings, counts, deletions, resets, and ChangesLanceDB filter handling
Priority: ⬆️ High Severity of issue fixed: High Merge Risk: ⚪ Minimal · up to The change hardens LanceDB filter construction and adds tests for tenant isolation and special characters. No merge-blocking risk was found in the supplied context. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change narrows the ability of crafted scopes or IDs to select unintended records. No new exposure was identified, but existing deletion paths do not consistently enforce a supplied scope, and database behavior has not been verified in this review. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Run each storage.delete() on its own line and assert on the returned count, so the deletion does not live inside an assert statement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Related issue
Fixes #5728
Summary
LanceDBStoragepasses its filters to LanceDBwhere(), which takes a raw SQL (DataFusion) expression with no parameter binding. Several filters were built by interpolating caller-supplied values without escaping:search(),_scan_rows()(used bylist_records,get_scope_info,count,list_categories, and category/metadata deletes),delete(scope_prefix=...)andreset(scope_prefix=...)delete(record_ids=...)and in the id list built by category/metadata deletesMemory.recall(),forget(),list_records(),info()andreset()forward an explicitscope=argument to these methods after only joining it underroot_scope. A scope value containing a quote could therefore change the predicate and read or delete records outside the configuredroot_scope, and%or_in a scope acted as LIKE wildcards. Legitimate scope names or ids containing an apostrophe failed with a SQL tokenizer error.Fix (one source file,
lancedb_storage.py):_sql_str()doubles single quotes. Every value interpolated into a filter now goes through it; the three call sites that already escaped ids inline now use the shared helper._id_in_filter()buildsid IN (...)from escaped ids (both delete paths andtouch_records)._scope_prefix_filter()escapes\,%and_and emitsscope LIKE '<escaped>%' ESCAPE '\', so only the trailing wildcard is a pattern character. Prefix-match semantics are otherwise unchanged.reset()escapes quotes in its range predicate.Compatibility. Lance accepts only backslash as the LIKE escape character (any other character is rejected at query time).
LIKE ... ESCAPE '\'was verified on lancedb 0.29.2 and 0.30.0, which is the full range allowed bylancedb>=0.29.2,<0.30.1, with and without the BTREE scope index, for scan queries, vector queries (prefilter and postfilter) and deletes. The only behavior change for existing callers:%or_inside a scope prefix now matches itself instead of acting as a wildcard, and a prefix ending in a backslash now works as a prefix.Input validation on explicit scopes: considered, not added. Scope strings come from users, from LLM-inferred scopes and from data already stored, and can legitimately contain characters outside a strict allowlist. With escaping at the sink every character is matched literally, so an allowlist would add breakage risk without closing any additional path.
Not changed here (pre-existing, separate behaviors):
/appalso matches/apple([BUG] [BUG] LanceDBStorage scope-prefix filters cross path boundaries:/appmatches/apple#7543, fix(memory): keep lancedb scope filters inside path boundaries #7544).delete(scope_prefix=...)also removes root-scope (/) records, and itsORis not parenthesized when combined witholder_than([BUG] LanceDBStorage.delete() ignores scope/older_than filters and causes accidental mass deletion when combining record_ids with categories #7419, fix(memory): honor combined LanceDB deletion filters #7472).Verification
New
lib/crewai/tests/memory/test_lancedb_filter_escaping.py(26 cases) runs against a real temporary LanceDB table holding two tenants' records:%, bare_,_inside a tenant name) cannot read tenant B throughsearch,list_recordsorcount, and cannot remove tenant B throughdelete, category-filtereddeleteorresetMemory(root_scope="/tenant-a"): crafted explicitscope=values passed torecall()andforget()stay inside tenant A-,_and/still match;%,_and\in scope names match literallyOn current
main22 of the 26 cases fail. The other four are the control case andresetwith wildcard payloads, whose range predicate never treated%or_as wildcards.Local checks (Python 3.13, macOS):
uv run pytest tests/memoryinlib/crewai: 179 passed (153 existing + 26 new); also 179 passed with lancedb 0.29.2uv run ruff check lib/anduv run ruff format --check lib/: cleanuv run mypy lib/: no issues in 930 source filesAdditional context
Reported by @shaysakazi. A security advisory will be published with the release that includes this fix.
Related open PRs, neither reviewed yet:
starts_with(scope, '<prefix>/'). On lancedb 0.29.2, which the current pin still allows, lance rewritesstarts_withinto an unescapedLIKE '<prefix>%':starts_with(scope, '/a_b')also matches/axb,/a%band/a\b, so%and_in the prefix still act as wildcards. 0.30.0 handles it correctly. For that reason this PR uses an explicitLIKE ... ESCAPEand does not take code from either PR. Their path-boundary and delete-filter fixes address separate behaviors and can build on_scope_prefix_filter()(for examplescope = '<p>' OR <_scope_prefix_filter('<p>/')>), or raise the lancedb floor to 0.30.0. Thanks to both authors for the analysis.🤖 Generated with Claude Code
Note
High Risk
Security fix for memory isolation: unescaped filters could allow cross-tenant read/delete via crafted scope or id strings passed through recall/forget and storage APIs.
Overview
Fixes SQL-style filter injection in
LanceDBStorage, where caller-supplied scope prefixes and record IDs were interpolated into LanceDBwhere()expressions without escaping. Crafted scopes could broaden reads/deletes across tenants;%and_in prefixes acted asLIKEwildcards; apostrophes in legitimate ids/scopes could break queries.The change centralizes escaping in
_sql_str,_id_in_filter, and_scope_prefix_filter(usingLIKE ... ESCAPE '\'so only the trailing%is a wildcard), and wires them through save/touch, search, scan, delete, and reset paths.reset()also escapes quotes in its range predicate.New integration tests in
test_lancedb_filter_escaping.pycover tenant isolation, crafted scopes/ids,Memorywithroot_scope, and literal matching for%,_,\, and quotes. Behavior change:%and_inside a scope prefix now match themselves, not wildcard semantics.Reviewed by Cursor Bugbot for commit 2692913. Bugbot is set up for automated code reviews on this repo. Configure here.