Skip to content

fix(search): literalize FTS5 MATCH tokens to prevent query semantics injection#313

Open
Arreboi06 wants to merge 3 commits into
TencentCloud:mainfrom
Arreboi06:fix/fts5-injection-issue-160
Open

fix(search): literalize FTS5 MATCH tokens to prevent query semantics injection#313
Arreboi06 wants to merge 3 commits into
TencentCloud:mainfrom
Arreboi06:fix/fts5-injection-issue-160

Conversation

@Arreboi06

@Arreboi06 Arreboi06 commented Jun 30, 2026

Copy link
Copy Markdown

Summary

Fixes #160 by changing buildFtsQuery() from an operator-sanitizing helper into a literal-token query builder.

The important security boundary is: user input is never treated as FTS5 syntax. User text is normalized into plain literal tokens, every emitted token is phrase-quoted, and the only MATCH operators inserted into the expression are generated by our code.

Relationship to #178

I saw the triage note that #178 is the locally verified canonical narrow fix for #160. This PR is now intentionally positioned as the stricter literal-token variant, not just another denylist patch.

The behavior beyond #178 is that reserved-looking words typed by the user are preserved as searchable text instead of being deleted:

AND OR NOT NEAR
  -> "AND" OR "OR" OR "NOT" OR "NEAR"

That means the fix prevents user-controlled FTS5 query structure while avoiding recall loss for documents that actually contain words such as AND, OR, NOT, or NEAR.

Core design decision

buildFtsQuery() owns the complete MATCH expression structure.

raw user text
  -> NFKC normalization
  -> Unicode literal-token extraction ([\p{L}\p{N}_]+)
  -> jieba search tokenization when available, regex fallback otherwise
  -> second literal-token safety pass for every tokenizer output
  -> quote every token as an FTS5 phrase literal
  -> join code-generated terms with code-generated OR

Inputs such as boolean operators, NEAR/5, column filters, quotes, wildcards, and groups can only become searchable text tokens. They cannot become FTS5 query control flow.

Why this is stronger than a denylist-only fix

A denylist-only fix removes known dangerous words such as AND, OR, NOT, and NEAR. That is a valid narrow fix, but it has two trade-offs:

  1. It can reduce recall by deleting words the user actually typed.
  2. It depends on recognizing every FTS5 syntax path that should be removed.

This PR avoids both: syntax-looking input is reduced to literal tokens, and every token is phrase-quoted before it reaches FTS5.

Security and recall behavior

User input Generated MATCH behavior Why it is safe
alpha" OR "beta "alpha" OR "OR" OR "beta" The injected OR is a quoted literal term, not user-controlled structure.
alpha AND NOT beta "alpha" OR "AND" OR "NOT" OR "beta" Boolean words stay searchable but cannot negate or combine terms.
(alpha) OR (beta) "alpha" OR "OR" OR "beta" Parentheses are separators, not grouping syntax.
content:foo "content" OR "foo" Column-filter syntax becomes plain text tokens.
alpha* "alpha" Prefix wildcard syntax cannot expand the query.
full-width AND "AND" NFKC normalization handles Unicode variants while preserving search text.

Implementation highlights

  • sanitizeFtsInput() normalizes raw input and extracts only Unicode literal tokens.
  • sanitizeFtsToken() re-sanitizes every tokenizer output so punctuation-bearing jieba/native/stub tokens cannot reintroduce MATCH syntax.
  • buildFtsQuery() contributes all MATCH structure itself; user text contributes tokens only.
  • Existing jieba-based Chinese recall remains in the path, with an additional literal-token safety pass after tokenization.
  • The latest commit removes the fragile internal string-split assembly and uses explicit quoted literal term arrays.

Test coverage

The tests cover generated query strings and real FTS5 execution:

  • Unit coverage for literalizing reserved words: AND, OR, NOT, NEAR.
  • Unit coverage for column-filter-looking input such as content:foo.
  • Unit coverage for quote injection, parenthesized expressions, prefix wildcard syntax, and NFKC normalization.
  • Tokenizer re-sanitization coverage for punctuation-bearing tokenizer output.
  • Real node:sqlite FTS5 fixture tests proving generated MATCH expressions execute successfully.
  • Real FTS5 fixture test proving reserved words remain searchable as literal document text.
  • Recall comparison against benign queries so ordinary keyword behavior is preserved.
  • 200 randomized adversarial query inputs to ensure generated MATCH expressions do not throw.

Verification

Latest pushed head:

802998c test(search): prove FTS5 literal token coverage

Full test suite:

npm.cmd test

Test Files  6 passed (6)
Tests       113 passed (113)

Build:

npm.cmd run build

Build complete

tsdown prints a Node.js v22.17.0 deprecation warning in this local environment, but the build exits successfully.

Files worth reviewing first

  1. src/core/store/sqlite.ts - literal-token query builder implementation.
  2. src/core/store/sqlite.test.ts - unit-level security, tokenizer re-sanitization, and recall semantics.
  3. src/core/store/sqlite.recall.test.ts - real FTS5 execution, reserved-word literal recall, recall parity, and fuzz coverage.
  4. CHANGELOG.md - documents the refined solution and its difference from fix(search): sanitize fts query operators #178.

Add defense-in-depth sanitization to `buildFtsQuery()` in
`src/core/store/sqlite.ts` so user input cannot alter FTS5 MATCH
semantics.

Pre-tokenization layer (sanitizeFtsInput)
- NFKC normalize input so full-width Unicode variants (e.g. full-width
  "AND") collapse to their canonical ASCII form before regex matching.
- Word-boundary strip FTS5 reserved operators AND / OR / NOT / NEAR
  (case-insensitive).  Word boundaries guarantee that substrings such
  as ANDROID, SCANNER, ORACLE, NEARBY survive untouched.
- Strip FTS5 syntax characters: ' " * ( ).
- Strip column-filter abuse including the negation prefix: content:,
  message:, session:, actor:, topic:, role:, tag:, user:, host:, file:.

Per-token layer (sanitizeFtsToken)
- NFKC normalize each emitted token.
- Keep only Unicode letters, digits, and underscore ([\p{L}\p{N}_]).
  Tokens that lose all such characters are dropped.
- Re-check against FTS5_RESERVED_OPS so jieba-produced tokens (a bare
  "OR", for example) cannot bypass the pre-tokenization filter.
- Wrap surviving tokens as FTS5 phrase strings with internal double-
  quote characters doubled per FTS5 escape rules.

Interface change
- buildFtsQuery now accepts null / undefined and returns null for empty
  input.  Downstream searchL1Fts / searchL0Fts already safely no-op
  when the MATCH argument is missing, so no caller changes are needed.

Tests
- 30 unit tests in src/core/store/sqlite.test.ts covering every FTS5
  reserved word (case + full-width variants), every syntax character,
  column-filter abuse including negation, word-boundary-safe substrings
  (ANDROID / ORACLE / NEARBY / SCANNER), empty / null / undefined input
  handling, jieba injection path simulation, and chained operator
  syntax.
- 14 integration tests in src/core/store/sqlite.recall.test.ts using a
  real node:sqlite in-memory FTS5 virtual table:
    - happy-path MATCH execution
    - all malicious inputs execute without throwing
    - 6-query recall parity suite (sanitized result set is a SUPERSET of
      legacy result set on benign queries)
    - 200-query fuzz against randomized adversarial inputs
- Full test suite passes (111 / 111, 6 test files).

Build
- tsdown build for the plugin (dist/index.mjs) succeeds.
- All auxiliary scripts (migrate-sqlite-to-tcvdb, export-tencent-vdb,
  read-local-memory) build clean.

Closes TencentCloud#160

Signed-off-by: Arreboi06 <3562419275@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@Arreboi06 Arreboi06 force-pushed the fix/fts5-injection-issue-160 branch from 4f9c5f0 to b4e3d56 Compare June 30, 2026 19:41
@Yuntong8888

Copy link
Copy Markdown
Collaborator

Hi @Arreboi06 👋,
Thank you for submitting this PR and participating in Tencent Rhino-bird Open-source Training Program!
We have successfully received your submission. The program is currently in full swing, and we will complete the Code Review for you as soon as possible. Please keep an eye on the status notifications for this PR so you can follow up promptly once the review feedback is provided.
Thanks again for your contribution and open-source spirit! 🚀

@Arreboi06 Arreboi06 changed the title fix(search): buildFtsQuery does not sanitize FTS5 operators — user input alters query semantics fix(search): literalize FTS5 MATCH tokens to prevent query semantics injection Jul 8, 2026
@Arreboi06

Copy link
Copy Markdown
Author

I pushed a follow-up refinement that strengthens the original fix.

The first version sanitized known FTS5 operators. The new version makes buildFtsQuery() a literal-token builder: user input is normalized and tokenized as plain text, every token is quoted as an FTS5 phrase, and only code-generated OR operators are used to assemble the MATCH expression.

This means inputs such as alpha" OR "beta, alpha AND NOT beta, (alpha) OR (beta), content:foo, and alpha* cannot alter MATCH semantics. At the same time, reserved words like AND / OR / NOT / NEAR remain searchable user terms instead of being dropped.

Verification:

npm.cmd test
# Test Files  6 passed (6)
# Tests       111 passed (111)
npm.cmd run build
# passed

@YOMXXX

YOMXXX commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Triage note: #178 has been locally verified and approved as the canonical narrow fix for #160. This PR appears to cover the same FTS5 operator-sanitization scope, so I recommend treating it as superseded unless it intentionally adds coverage or behavior beyond #178.

@Arreboi06

Arreboi06 commented Jul 9, 2026

Copy link
Copy Markdown
Author

Following up on the triage note that #178 is the canonical narrow fix for #160: I pushed 802998c to make this PR intentionally cover behavior beyond a denylist-only operator-sanitization patch.

What is now distinct from #178:

  • buildFtsQuery() is framed as a literal-token query builder: user input can contribute searchable text only, never FTS5 query structure.
  • Reserved-looking user terms such as AND, OR, NOT, and NEAR are preserved as quoted literals instead of being deleted, so the security fix does not reduce recall for documents that actually contain those words.
  • Tokenizer output is re-sanitized after jieba/fallback tokenization, so punctuation-bearing native/stub tokenizer output cannot reintroduce MATCH syntax.
  • The test coverage now includes true node:sqlite FTS5 execution, reserved-word literal recall, tokenizer re-sanitization, recall parity, and 200 randomized adversarial inputs.

Fresh local verification on the pushed head:

npm.cmd test
Test Files  6 passed (6)
Tests       113 passed (113)
image
npm.cmd run build
Build complete
image
tokenizer 二次净化测试
真实 FTS5 执行测试
image
tokenizer 二次净化测试
re-sanitizes tokenizer output before building MATCH
image image
真实 FTS5 执行测试
image image

So I agree #178 remains the canonical narrow fix. This PR is now intended as the stricter literal-token variant with additional behavior and coverage, if the maintainers prefer that shape for #160.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(search): buildFtsQuery does not sanitize FTS5 operators — user input alters query semantics

3 participants