Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ The snapshot files are named `snapshot-<timestamp>.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.
20 changes: 20 additions & 0 deletions search.go
Original file line number Diff line number Diff line change
@@ -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), " ")
}
33 changes: 33 additions & 0 deletions search_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}