From 4724d14a3741ccf05d03e8b05e37b84d6a047aed Mon Sep 17 00:00:00 2001 From: Seth Hoenig Date: Sun, 10 May 2026 08:55:35 -0500 Subject: [PATCH] litesql: implement fts5 sanitization function --- README.md | 12 ++++++++++++ search.go | 20 ++++++++++++++++++++ search_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 search.go create mode 100644 search_test.go diff --git a/README.md b/README.md index acd36ea..872f7d5 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,18 @@ The snapshot files are named `snapshot-.db` in the specified directory, and older files are automatically cleaned up according to the `Retention` policy. +#### Sanitizing FTS5 queries + +SQLite FTS5 uses special control characters (`*`, `:`, `^`, `"`, etc.) that can +cause query errors or allow malicious query injection. Use `SanitizeFTS5` to +strip these characters before passing user input to an FTS5 query. + +```go +query := "hello world*" // prefix-operator performance implications +sanitized := litesql.SanitizeFTS5(query) // removes fts5 control characters +fmt.Println(sanitized) // "hello world" +``` + ### License The `cattlecloud.net/go/litesql` module is opensource under the [BSD-3-Clause](LICENSE) license. diff --git a/search.go b/search.go new file mode 100644 index 0000000..f766ff1 --- /dev/null +++ b/search.go @@ -0,0 +1,20 @@ +package litesql + +import ( + "strings" + "unicode" +) + +// SanitizeFTS5 removes SQLite FTS5 control characters (*, :, ^, ", etc.) +// by retaining only alphanumeric characters and basic whitespace. +func SanitizeFTS5(query string) string { + sanitized := strings.Map(func(r rune) rune { + if unicode.IsLetter(r) || unicode.IsNumber(r) || unicode.IsSpace(r) { + return r + } + return -1 // drop + }, query) + + // clean up duplicate spaces and trim padding + return strings.Join(strings.Fields(sanitized), " ") +} diff --git a/search_test.go b/search_test.go new file mode 100644 index 0000000..0de971f --- /dev/null +++ b/search_test.go @@ -0,0 +1,33 @@ +package litesql + +import ( + "testing" + + "github.com/shoenig/test/must" +) + +func Test_SanitizeFTS5(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + input string + exp string + }{ + {"asterisk removed", "foo*bar", "foobar"}, + {"colon removed", "foo:bar", "foobar"}, + {"caret removed", "foo^bar", "foobar"}, + {"quote removed", `foo"bar`, "foobar"}, + {"acceptance", "hello world:foo*bar ^baz", "hello worldfoobar baz"}, + {"empty string", "", ""}, + {"no sanitization needed", "simple query", "simple query"}, + {"multiple spaces normalized", "foo bar baz", "foo bar baz"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := SanitizeFTS5(tc.input) + must.Eq(t, tc.exp, result) + }) + } +}