From de5685157b99c239e55c22ad34b04974c01f25e7 Mon Sep 17 00:00:00 2001 From: Radegast Date: Mon, 18 Nov 2013 12:06:51 +0000 Subject: [PATCH] lua: (xp save) prevent sql injections with modified cl_guid, refs #234 --- xpsave/xpsave.lua | 102 ++++++++++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/xpsave/xpsave.lua b/xpsave/xpsave.lua index 72bdfa1..fed4943 100755 --- a/xpsave/xpsave.lua +++ b/xpsave/xpsave.lua @@ -4,26 +4,36 @@ License: MIT Released on 17.11.2013 Website: http://www.etlegacy.com + Mod: intended for Legacy, but might also work in NoQuarter Description: this script saves users' experience points into a database and thus preserves them between connections ]]-- -- load sqlite driver (or mysql..) -luasql = require "luasql.sqlite3" +local luasql = require "luasql.sqlite3" local env -- environment object local con -- database connection local cur -- cursor +-- skill identifiers +local BATTLESENSE = 0 +local ENGINEERING = 1 +local MEDIC = 2 +local FIELDOPS = 3 +local LIGHTWEAPONS = 4 +local HEAVYWEAPONS = 5 +local COVERTOPS = 6 + local skills = {} -skills[0] = "Battlesense" -skills[1] = "Engineering" -skills[2] = "Medic" -skills[3] = "Field ops" -skills[4] = "Light weapons" -skills[5] = "Heavy weapons" -skills[6] = "Covert ops" +skills[BATTLESENSE] = "Battlesense" +skills[ENGINEERING] = "Engineering" +skills[MEDIC] = "Medic" +skills[FIELDOPS] = "Field ops" +skills[LIGHTWEAPONS] = "Light weapons" +skills[HEAVYWEAPONS] = "Heavy weapons" +skills[COVERTOPS] = "Covert ops" -- database helper function -- returns database rows matching sql_statement @@ -34,6 +44,20 @@ function rows(connection, sql_statement) end end -- rows +-- con:prepare with bind_names should be used to prevent sql injections +-- but it doesn't work on my version of luasql +function validateGUID(cno, guid) + -- allow only alphanumeric characters in guid + if(string.match(guid, "%W")) then + -- Invalid characters detected. We should probably drop this client + et.G_Print("^3WARNING: (XP Save) user with id " .. cno .. " has an invalid GUID: " .. guid .. "\n") + et.trap_SendServerCommand (cno, "cpm \"" .. "Your XP won't be saved because you have an invalid cl_guid.\n\"") + return false + end + + return true +end + -- init db on game start function et_InitGame(levelTime, randomSeed, restart) -- name of this module @@ -63,6 +87,15 @@ function et_InitGame(levelTime, randomSeed, restart) UNIQUE (guid) ) ]]) + + cur = assert ( con:execute("SELECT COUNT(*) FROM users") ) + + et.G_Print("There are " .. tonumber(cur:fetch(row, 'a')) .. " users in the XP save database.\n") + + --et.G_Print ("^4List of users in XP Save database:\n") + --for guid, date in rows (con, "SELECT * FROM users") do + -- et.G_Print (string.format ("\tGUID %s was last seen on %s\n", guid, date)) + --end end -- et_InitGame function et_ShutdownGame(restart) @@ -74,47 +107,46 @@ end -- et_ShutdownGame -- called when a client enters the game world function et_ClientBegin(cno) - name = et.Info_ValueForKey( et.trap_GetUserinfo( cno ), "name" ) - guid = et.Info_ValueForKey( et.trap_GetUserinfo( cno ), "cl_guid" ) + local name = et.Info_ValueForKey( et.trap_GetUserinfo( cno ), "name" ) + local guid = et.Info_ValueForKey( et.trap_GetUserinfo( cno ), "cl_guid" ) + if not validateGUID(cno, guid) then return end + cur = assert (con:execute(string.format("SELECT * FROM users WHERE guid='%s'", guid))) - player = cur:fetch({}, 'a') + local player = cur:fetch({}, 'a') if not player then -- First time we see this player - et.trap_SendServerCommand (cno, "cpm \"" .. "Welcome, " .. name .. "!\n\"") + et.trap_SendServerCommand (cno, "cpm \"" .. "Welcome, " .. name .. "! You are playing on an XP save server.\n\"") cur = assert (con:execute(string.format("INSERT INTO users VALUES ('%s', '%s', 0, 0, 0, 0, 0, 0, 0)", guid, os.date("%Y-%m-%d %H:%M:%S")))) else et.trap_SendServerCommand (cno, "cpm \"" .. "Welcome back, " .. name .. "! Your last connection was on " .. player.last_seen .. "\n\"") -- in db: player.name --et.G_Print ("Loading XP from database: " .. player.xp_battlesense .. " | " .. player.xp_engineering .. " | " .. player.xp_medic .. " | " .. player.xp_fieldops .. " | " .. player.xp_lightweapons .. " | " .. player.xp_heavyweapons .. " | " .. player.xp_covertops .. "\n\n") - - et.G_XP_Set (cno, player.xp_battlesense, 0, 0) - et.G_XP_Set (cno, player.xp_engineering, 1, 0) - et.G_XP_Set (cno, player.xp_medic, 2, 0) - et.G_XP_Set (cno, player.xp_fieldops, 3, 0) - et.G_XP_Set (cno, player.xp_lightweapons, 4, 0) - et.G_XP_Set (cno, player.xp_heavyweapons, 5, 0) - et.G_XP_Set (cno, player.xp_covertops, 6, 0) + + et.G_XP_Set (cno, player.xp_battlesense, BATTLESENSE, 0) + et.G_XP_Set (cno, player.xp_engineering, ENGINEERING, 0) + et.G_XP_Set (cno, player.xp_medic, MEDIC, 0) + et.G_XP_Set (cno, player.xp_fieldops, FIELDOPS, 0) + et.G_XP_Set (cno, player.xp_lightweapons, LIGHTWEAPONS, 0) + et.G_XP_Set (cno, player.xp_heavyweapons, HEAVYWEAPONS, 0) + et.G_XP_Set (cno, player.xp_covertops, COVERTOPS, 0) et.G_Print (name .. "'s current XP levels:\n") for id, skill in pairs(skills) do et.G_Print ("\t" .. skill .. ": " .. et.gentity_get (cno, "sess.skillpoints", id) .. " XP\n") end end - - --et.G_Print ("^4List of users in database:\n") - --for name, guid, date in rows (con, "SELECT * FROM users") do - -- et.G_Print (string.format ("\t%s with guid %s last seen on %s\n", name, guid, date)) - --end end -- et_ClientBegin function et_ClientDisconnect(cno) - name = et.Info_ValueForKey( et.trap_GetUserinfo( cno ), "name" ) - guid = et.Info_ValueForKey( et.trap_GetUserinfo( cno ), "cl_guid" ) + local name = et.Info_ValueForKey( et.trap_GetUserinfo( cno ), "name" ) + local guid = et.Info_ValueForKey( et.trap_GetUserinfo( cno ), "cl_guid" ) + + if not validateGUID(cno, guid) then return end cur = assert (con:execute(string.format("SELECT * FROM users WHERE guid='%s' LIMIT 1", guid))) - player = cur:fetch({}, 'a') + local player = cur:fetch({}, 'a') if not player then -- This should not happen @@ -135,13 +167,13 @@ function et_ClientDisconnect(cno) xp_covertops='%s' WHERE guid='%s']], os.date("%Y-%m-%d %H:%M:%S"), - et.gentity_get (cno, "sess.skillpoints", 0), - et.gentity_get (cno, "sess.skillpoints", 1), - et.gentity_get (cno, "sess.skillpoints", 2), - et.gentity_get (cno, "sess.skillpoints", 3), - et.gentity_get (cno, "sess.skillpoints", 4), - et.gentity_get (cno, "sess.skillpoints", 5), - et.gentity_get (cno, "sess.skillpoints", 6), + et.gentity_get (cno, "sess.skillpoints", BATTLESENSE), + et.gentity_get (cno, "sess.skillpoints", ENGINEERING), + et.gentity_get (cno, "sess.skillpoints", MEDIC), + et.gentity_get (cno, "sess.skillpoints", FIELDOPS), + et.gentity_get (cno, "sess.skillpoints", LIGHTWEAPONS), + et.gentity_get (cno, "sess.skillpoints", HEAVYWEAPONS), + et.gentity_get (cno, "sess.skillpoints", COVERTOPS), guid ))) end