diff --git a/lib/sql/server/sqlHandler.lua b/lib/sql/server/sqlHandler.lua index 644467e..9e35fec 100644 --- a/lib/sql/server/sqlHandler.lua +++ b/lib/sql/server/sqlHandler.lua @@ -1,21 +1,67 @@ +--[[ + DEPRECATED: the SQL helper module is deprecated in its entirety and will be + REMOVED on 2026-08-22. Migrate to oxmysql directly — MySQL.query.await(query, + params) with ? placeholders — which is safer and more flexible than these + wrappers ever were. +]] + SQL = {} Require("lib/MySQL.lua", "oxmysql") +local warnedResources = {} + +local function warnDeprecated() + local caller = GetInvokingResource() or GetCurrentResourceName() + if warnedResources[caller] then return end + warnedResources[caller] = true + print(("^3[community_bridge] SQL helpers are deprecated and will be REMOVED on 2026-08-22. '%s' should migrate to oxmysql directly (MySQL.query.await with ? placeholders).^0"):format(caller)) +end + +-- Identifiers (table/column names) cannot be parameterized, so they are +-- validated against a strict pattern and backtick-quoted instead. Values are +-- never concatenated into queries - they always go through ? placeholders. +local function quoteIdentifier(name) + assert(type(name) == "string" and name:match("^[%a_][%w_]*$"), + ("SQL: unsafe identifier '%s' - table/column names may only contain letters, digits and underscores"):format(tostring(name))) + return "`" .. name .. "`" +end + +--- Builds a parameterized WHERE clause from a table of column = value pairs. +-- Raw WHERE strings are intentionally rejected: they made SQL injection +-- unavoidable whenever a player-influenced value reached these helpers. +local function buildWhere(where) + assert(type(where) == "table" and next(where) ~= nil, + "SQL: where must be a non-empty table of column = value pairs (raw WHERE strings are no longer accepted)") + + local conditions = {} + local params = {} + + for column, value in pairs(where) do + table.insert(conditions, quoteIdentifier(column) .. " = ?") + table.insert(params, value) + end + + return table.concat(conditions, " AND "), params +end + --- Creates a table in the database if it does not exist. -- @param tableName The name of the table to create. Example: {{ name = "identifier", type = "VARCHAR(50)", primary = true }} -- @param columns A table containing column definitions, where each column is a table with 'name' and 'type'. ---@return nil +---@deprecated Removed on 2026-08-22 — use oxmysql directly. function SQL.Create(tableName, columns) + warnDeprecated() assert(MySQL, "Tried using module SQL without MySQL being loaded") local columnsList = {} for i, column in pairs(columns) do - table.insert(columnsList, string.format("%s %s", column.name, column.type)) + assert(type(column.type) == "string", "SQL: column type must be a string") + table.insert(columnsList, string.format("%s %s", quoteIdentifier(column.name), column.type)) end local query = string.format("CREATE TABLE IF NOT EXISTS %s (%s);", - tableName, + quoteIdentifier(tableName), table.concat(columnsList, ", ") ) @@ -23,50 +69,70 @@ function SQL.Create(tableName, columns) end -- insert if not exist otherwise update +---@deprecated Removed on 2026-08-22 — use oxmysql directly. function SQL.InsertOrUpdate(tableName, data) + warnDeprecated() assert(MySQL, "Tried using module SQL without MySQL being loaded") local columns = {} - local values = {} + local placeholders = {} local updates = {} + local params = {} for column, value in pairs(data) do - table.insert(columns, column) - table.insert(values, "'" .. value .. "'") -- Ensure values are properly quoted - table.insert(updates, column .. " = VALUES(" .. column .. ")") -- Use VALUES() for update + local quoted = quoteIdentifier(column) + table.insert(columns, quoted) + table.insert(placeholders, "?") + table.insert(updates, quoted .. " = VALUES(" .. quoted .. ")") -- Use VALUES() for update + table.insert(params, value) end local query = string.format( "INSERT INTO %s (%s) VALUES (%s) ON DUPLICATE KEY UPDATE %s;", - tableName, + quoteIdentifier(tableName), table.concat(columns, ", "), - table.concat(values, ", "), + table.concat(placeholders, ", "), table.concat(updates, ", ") ) - MySQL.query.await(query) + MySQL.query.await(query, params) end +--- Selects rows matching the given filters. +-- @param tableName The table to query. +-- @param where Table of column = value pairs, joined with AND. Example: { citizenid = cid } +---@deprecated Removed on 2026-08-22 — use oxmysql directly. function SQL.Get(tableName, where) + warnDeprecated() assert(MySQL, "Tried using module SQL without MySQL being loaded") - local query = string.format("SELECT * FROM %s WHERE %s;", tableName, where) - local result = MySQL.query.await(query) + local conditions, params = buildWhere(where) + local query = string.format("SELECT * FROM %s WHERE %s;", quoteIdentifier(tableName), conditions) + local result = MySQL.query.await(query, params) return result end +---@deprecated Removed on 2026-08-22 — use oxmysql directly. function SQL.GetAll(tableName) + warnDeprecated() assert(MySQL, "Tried using module SQL without MySQL being loaded") - local query = string.format("SELECT * FROM %s;", tableName) + local query = string.format("SELECT * FROM %s;", quoteIdentifier(tableName)) local result = MySQL.query.await(query) return result end +--- Deletes rows matching the given filters. +-- @param tableName The table to delete from. +-- @param where Table of column = value pairs, joined with AND. Example: { citizenid = cid } +---@deprecated Removed on 2026-08-22 — use oxmysql directly. function SQL.Delete(tableName, where) + warnDeprecated() assert(MySQL, "Tried using module SQL without MySQL being loaded") - local query = string.format("DELETE FROM %s WHERE %s;", tableName, where) - MySQL.query.await(query) + local conditions, params = buildWhere(where) + local query = string.format("DELETE FROM %s WHERE %s;", quoteIdentifier(tableName), conditions) + MySQL.query.await(query, params) end exports('SQL', function() + warnDeprecated() return SQL end)