@@ -24,11 +24,13 @@ MySQL_FTS::~MySQL_FTS() {
2424int MySQL_FTS::init () {
2525 // Initialize database connection
2626 db = new SQLite3DB ();
27- char path_buf[ db_path.size () + 1 ] ;
28- strcpy (path_buf, db_path.c_str ());
29- int rc = db->open (path_buf, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
27+ std::vector< char > path_buf ( db_path.size () + 1 ) ;
28+ strcpy (path_buf. data () , db_path.c_str ());
29+ int rc = db->open (path_buf. data () , SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
3030 if (rc != SQLITE_OK) {
3131 proxy_error (" Failed to open FTS database at %s: %d\n " , db_path.c_str (), rc);
32+ delete db;
33+ db = NULL ;
3234 return -1 ;
3335 }
3436
@@ -88,16 +90,36 @@ int MySQL_FTS::create_tables() {
8890}
8991
9092std::string MySQL_FTS::sanitize_name (const std::string& name) {
91- std::string sanitized = name;
92- // Replace dots and special characters with underscores
93- for (size_t i = 0 ; i < sanitized.length (); i++) {
94- if (sanitized[i] == ' .' || sanitized[i] == ' -' || sanitized[i] == ' ' ) {
95- sanitized[i] = ' _' ;
93+ const size_t MAX_NAME_LEN = 100 ;
94+ std::string sanitized;
95+ // Allowlist: only ASCII letters, digits, underscore
96+ for (char c : name) {
97+ if ((c >= ' a' && c <= ' z' ) || (c >= ' A' && c <= ' Z' ) ||
98+ (c >= ' 0' && c <= ' 9' ) || c == ' _' ) {
99+ sanitized.push_back (c);
96100 }
97101 }
102+ // Prevent leading digit (SQLite identifiers can't start with digit)
103+ if (!sanitized.empty () && sanitized[0 ] >= ' 0' && sanitized[0 ] <= ' 9' ) {
104+ sanitized.insert (sanitized.begin (), ' _' );
105+ }
106+ // Enforce maximum length
107+ if (sanitized.length () > MAX_NAME_LEN) sanitized = sanitized.substr (0 , MAX_NAME_LEN);
98108 return sanitized;
99109}
100110
111+ std::string MySQL_FTS::escape_identifier (const std::string& identifier) {
112+ std::string escaped;
113+ escaped.reserve (identifier.length () * 2 + 2 );
114+ escaped.push_back (' `' );
115+ for (char c : identifier) {
116+ escaped.push_back (c);
117+ if (c == ' `' ) escaped.push_back (' `' ); // Double backticks
118+ }
119+ escaped.push_back (' `' );
120+ return escaped;
121+ }
122+
101123std::string MySQL_FTS::escape_sql (const std::string& str) {
102124 std::string escaped;
103125 for (size_t i = 0 ; i < str.length (); i++) {
@@ -148,12 +170,12 @@ bool MySQL_FTS::index_exists(const std::string& schema, const std::string& table
148170int MySQL_FTS::create_index_tables (const std::string& schema, const std::string& table) {
149171 std::string data_table = get_data_table_name (schema, table);
150172 std::string fts_table = get_fts_table_name (schema, table);
151- std::string sanitized_data = data_table;
152- std::string sanitized_fts = fts_table;
173+ std::string escaped_data = escape_identifier ( data_table) ;
174+ std::string escaped_fts = escape_identifier ( fts_table) ;
153175
154176 // Create data table
155177 std::ostringstream create_data_sql;
156- create_data_sql << " CREATE TABLE IF NOT EXISTS " << sanitized_data << " ("
178+ create_data_sql << " CREATE TABLE IF NOT EXISTS " << escaped_data << " ("
157179 " rowid INTEGER PRIMARY KEY AUTOINCREMENT,"
158180 " schema_name TEXT NOT NULL,"
159181 " table_name TEXT NOT NULL,"
@@ -169,9 +191,9 @@ int MySQL_FTS::create_index_tables(const std::string& schema, const std::string&
169191
170192 // Create FTS5 virtual table with external content
171193 std::ostringstream create_fts_sql;
172- create_fts_sql << " CREATE VIRTUAL TABLE IF NOT EXISTS " << sanitized_fts << " USING fts5("
194+ create_fts_sql << " CREATE VIRTUAL TABLE IF NOT EXISTS " << escaped_fts << " USING fts5("
173195 " content, metadata,"
174- " content=' " << sanitized_data << " ' ,"
196+ " content=" << escaped_data << " ,"
175197 " content_rowid='rowid',"
176198 " tokenize='porter unicode61'"
177199 " );" ;
@@ -183,37 +205,38 @@ int MySQL_FTS::create_index_tables(const std::string& schema, const std::string&
183205
184206 // Create triggers for automatic sync (populate the FTS table)
185207 std::string base_name = sanitize_name (schema) + " _" + sanitize_name (table);
208+ std::string escaped_base = escape_identifier (base_name);
186209
187210 // Drop existing triggers if any
188- db->execute ((" DROP TRIGGER IF EXISTS fts_ai_" + base_name).c_str ());
189- db->execute ((" DROP TRIGGER IF EXISTS fts_ad_" + base_name).c_str ());
190- db->execute ((" DROP TRIGGER IF EXISTS fts_au_" + base_name).c_str ());
211+ db->execute ((" DROP TRIGGER IF EXISTS " + escape_identifier ( " fts_ai_" + base_name) ).c_str ());
212+ db->execute ((" DROP TRIGGER IF EXISTS " + escape_identifier ( " fts_ad_" + base_name) ).c_str ());
213+ db->execute ((" DROP TRIGGER IF EXISTS " + escape_identifier ( " fts_au_" + base_name) ).c_str ());
191214
192215 // AFTER INSERT trigger
193216 std::ostringstream ai_sql;
194- ai_sql << " CREATE TRIGGER IF NOT EXISTS fts_ai_ " << base_name
195- << " AFTER INSERT ON " << sanitized_data << " BEGIN"
196- << " INSERT INTO " << sanitized_fts << " (rowid, content, metadata)"
217+ ai_sql << " CREATE TRIGGER IF NOT EXISTS " << escape_identifier ( " fts_ai_ " + base_name)
218+ << " AFTER INSERT ON " << escaped_data << " BEGIN"
219+ << " INSERT INTO " << escaped_fts << " (rowid, content, metadata)"
197220 << " VALUES (new.rowid, new.content, new.metadata);"
198221 << " END;" ;
199222 db->execute (ai_sql.str ().c_str ());
200223
201224 // AFTER DELETE trigger
202225 std::ostringstream ad_sql;
203- ad_sql << " CREATE TRIGGER IF NOT EXISTS fts_ad_ " << base_name
204- << " AFTER DELETE ON " << sanitized_data << " BEGIN"
205- << " INSERT INTO " << sanitized_fts << " (" << sanitized_fts << " , rowid, content, metadata)"
226+ ad_sql << " CREATE TRIGGER IF NOT EXISTS " << escape_identifier ( " fts_ad_ " + base_name)
227+ << " AFTER DELETE ON " << escaped_data << " BEGIN"
228+ << " INSERT INTO " << escaped_fts << " (" << escaped_fts << " , rowid, content, metadata)"
206229 << " VALUES ('delete', old.rowid, old.content, old.metadata);"
207230 << " END;" ;
208231 db->execute (ad_sql.str ().c_str ());
209232
210233 // AFTER UPDATE trigger
211234 std::ostringstream au_sql;
212- au_sql << " CREATE TRIGGER IF NOT EXISTS fts_au_ " << base_name
213- << " AFTER UPDATE ON " << sanitized_data << " BEGIN"
214- << " INSERT INTO " << sanitized_fts << " (" << sanitized_fts << " , rowid, content, metadata)"
235+ au_sql << " CREATE TRIGGER IF NOT EXISTS " << escape_identifier ( " fts_au_ " + base_name)
236+ << " AFTER UPDATE ON " << escaped_data << " BEGIN"
237+ << " INSERT INTO " << escaped_fts << " (" << escaped_fts << " , rowid, content, metadata)"
215238 << " VALUES ('delete', old.rowid, old.content, old.metadata);"
216- << " INSERT INTO " << sanitized_fts << " (rowid, content, metadata)"
239+ << " INSERT INTO " << escaped_fts << " (rowid, content, metadata)"
217240 << " VALUES (new.rowid, new.content, new.metadata);"
218241 << " END;" ;
219242 db->execute (au_sql.str ().c_str ());
@@ -327,7 +350,7 @@ std::string MySQL_FTS::index_table(
327350
328351 // Get data table name
329352 std::string data_table = get_data_table_name (schema, table);
330- std::string sanitized_data = data_table;
353+ std::string escaped_data = escape_identifier ( data_table) ;
331354
332355 // Insert data in batches
333356 int row_count = 0 ;
@@ -371,7 +394,7 @@ std::string MySQL_FTS::index_table(
371394
372395 // Insert into data table (triggers will sync to FTS)
373396 std::ostringstream insert_sql;
374- insert_sql << " INSERT INTO " << sanitized_data
397+ insert_sql << " INSERT INTO " << escaped_data
375398 << " (schema_name, table_name, primary_key_value, content, metadata) "
376399 << " VALUES ('" << escape_sql (schema) << " ', '"
377400 << escape_sql (table) << " ', '"
@@ -483,17 +506,26 @@ std::string MySQL_FTS::search(
483506
484507 std::string data_table = get_data_table_name (idx_schema, idx_table);
485508 std::string fts_table = get_fts_table_name (idx_schema, idx_table);
486- std::string sanitized_data = data_table;
509+ std::string escaped_data = escape_identifier (data_table);
510+ std::string escaped_fts = escape_identifier (fts_table);
511+
512+ // Escape query for FTS5 MATCH clause (wrap in double quotes, escape embedded quotes)
513+ std::string fts_literal = " \" " ;
514+ for (char c : query) {
515+ fts_literal.push_back (c);
516+ if (c == ' "' ) fts_literal.push_back (' "' ); // Double quotes
517+ }
518+ fts_literal.push_back (' "' );
487519
488520 // Search query for this index (use table name for MATCH/bm25)
489521 std::ostringstream search_sql;
490522 search_sql << " SELECT d.schema_name, d.table_name, d.primary_key_value, "
491- << " snippet(" << fts_table << " , 0, '<mark>', '</mark>', '...', 30) AS snippet, "
523+ << " snippet(" << escaped_fts << " , 0, '<mark>', '</mark>', '...', 30) AS snippet, "
492524 << " d.metadata "
493- << " FROM " << fts_table << " "
494- << " JOIN " << sanitized_data << " d ON " << fts_table << " .rowid = d.rowid "
495- << " WHERE " << fts_table << " MATCH ' " << escape_sql (query) << " ' "
496- << " ORDER BY bm25(" << fts_table << " ) ASC "
525+ << " FROM " << escaped_fts << " "
526+ << " JOIN " << escaped_data << " d ON " << escaped_fts << " .rowid = d.rowid "
527+ << " WHERE " << escaped_fts << " MATCH " << fts_literal << " "
528+ << " ORDER BY bm25(" << escaped_fts << " ) ASC "
497529 << " LIMIT " << limit;
498530
499531 SQLite3_result* idx_resultset = NULL ;
@@ -581,6 +613,7 @@ std::string MySQL_FTS::list_indexes() {
581613
582614 if (error) {
583615 result[" error" ] = " Failed to list indexes: " + std::string (error);
616+ (*proxy_sqlite3_free)(error);
584617 return result.dump ();
585618 }
586619
@@ -633,17 +666,17 @@ std::string MySQL_FTS::delete_index(const std::string& schema, const std::string
633666 db->wrlock ();
634667
635668 // Drop triggers
636- db->execute ((" DROP TRIGGER IF EXISTS fts_ai_" + base_name).c_str ());
637- db->execute ((" DROP TRIGGER IF EXISTS fts_ad_" + base_name).c_str ());
638- db->execute ((" DROP TRIGGER IF EXISTS fts_au_" + base_name).c_str ());
669+ db->execute ((" DROP TRIGGER IF EXISTS " + escape_identifier ( " fts_ai_" + base_name) ).c_str ());
670+ db->execute ((" DROP TRIGGER IF EXISTS " + escape_identifier ( " fts_ad_" + base_name) ).c_str ());
671+ db->execute ((" DROP TRIGGER IF EXISTS " + escape_identifier ( " fts_au_" + base_name) ).c_str ());
639672
640673 // Drop FTS table
641674 std::string fts_table = get_fts_table_name (schema, table);
642- db->execute ((" DROP TABLE IF EXISTS " + fts_table).c_str ());
675+ db->execute ((" DROP TABLE IF EXISTS " + escape_identifier ( fts_table) ).c_str ());
643676
644677 // Drop data table
645678 std::string data_table = get_data_table_name (schema, table);
646- db->execute ((" DROP TABLE IF EXISTS " + data_table).c_str ());
679+ db->execute ((" DROP TABLE IF EXISTS " + escape_identifier ( data_table) ).c_str ());
647680
648681 // Remove metadata
649682 std::ostringstream metadata_sql;
@@ -751,7 +784,7 @@ std::string MySQL_FTS::rebuild_all(MySQL_Tool_Handler* mysql_handler) {
751784 json failed_item;
752785 failed_item[" schema" ] = schema;
753786 failed_item[" table" ] = table;
754- failed_item[" error" ] = reindex_json[ " error" ]. get < std::string>( );
787+ failed_item[" error" ] = reindex_json. value ( " error" , std::string ( " unknown error " ) );
755788 failed.push_back (failed_item);
756789 }
757790 }
0 commit comments