From 32ed879d2c2edb8b97c8b6339ea81609bc4c91a0 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Thu, 3 Sep 2026 10:54:19 +0200 Subject: [PATCH 1/2] feat: add SBPP_BanPlayerBySteamId native for offline player bans Ports upstream sbpp/sourcebans-pp#1511 onto the srcdslab fork. Adds SBPP_BanPlayerBySteamId, a native for banning players who are no longer connected to the server (TK managers, anti-cheat, etc.). It accepts a SteamID2 string and player name directly instead of a client index. - Validates SteamID2 format before inserting - Runs a duplicate-check SELECT first, skipping the INSERT on an active ban - Fires SBPP_OnBanPlayer with iTarget = -1 only after the INSERT succeeds - Falls back to a server-lookup subquery for sid when serverID == -1 Fork adaptation: the INSERT keeps the fork's admin_name column and the IFNULL((SELECT user FROM %s_admins ...)) subquery, matching the fork's UTIL_InsertBan / Native_SBBanPlayer statements. Co-Authored-By: Claude Sonnet 5 Co-authored-by: Sigibert --- .../scripting/include/sourcebanspp.inc | 15 ++ game/addons/sourcemod/scripting/sbpp_main.sp | 147 ++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/game/addons/sourcemod/scripting/include/sourcebanspp.inc b/game/addons/sourcemod/scripting/include/sourcebanspp.inc index f29563c3e..4f534b7c2 100644 --- a/game/addons/sourcemod/scripting/include/sourcebanspp.inc +++ b/game/addons/sourcemod/scripting/include/sourcebanspp.inc @@ -81,6 +81,7 @@ public void __pl_sourcebanspp_SetNTVOptional() { MarkNativeAsOptional("SBBanPlayer"); MarkNativeAsOptional("SBPP_BanPlayer"); + MarkNativeAsOptional("SBPP_BanPlayerBySteamId"); MarkNativeAsOptional("SBPP_ReportPlayer"); } #endif @@ -109,6 +110,20 @@ native void SBBanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason *********************************************************/ native void SBPP_BanPlayer(int iAdmin, int iTarget, int iTime, const char[] sReason); +/********************************************************* + * Ban a player by SteamID — supports offline/disconnected players. + * Inserts the ban directly into the SourceBans++ database. + * Fires SBPP_OnBanPlayer with iTarget = -1 to indicate an offline ban. + * + * @param iAdmin Client index of the admin (0 = server / automated) + * @param steamId SteamID string, e.g. "STEAM_0:0:12345" + * @param name Player name; may be empty string if unknown + * @param iTime Ban duration in minutes (0 = permanent) + * @param sReason Reason for the ban + * @noreturn + *********************************************************/ +native void SBPP_BanPlayerBySteamId(int iAdmin, const char[] steamId, const char[] name, int iTime, const char[] sReason); + /********************************************************* * Reports a player * diff --git a/game/addons/sourcemod/scripting/sbpp_main.sp b/game/addons/sourcemod/scripting/sbpp_main.sp index 75746aa6c..4febb5819 100644 --- a/game/addons/sourcemod/scripting/sbpp_main.sp +++ b/game/addons/sourcemod/scripting/sbpp_main.sp @@ -144,6 +144,7 @@ public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max CreateNative("SBBanPlayer", Native_SBBanPlayer); CreateNative("SBPP_BanPlayer", Native_SBBanPlayer); + CreateNative("SBPP_BanPlayerBySteamId", Native_SBPP_BanPlayerBySteamId); CreateNative("SBPP_ReportPlayer", Native_SBReportPlayer); g_hFwd_OnBanAdded = CreateGlobalForward("SBPP_OnBanPlayer", ET_Ignore, Param_Cell, Param_Cell, Param_Cell, Param_String); @@ -2386,6 +2387,152 @@ public int Native_SBBanPlayer(Handle plugin, int numParams) return true; } +public int Native_SBPP_BanPlayerBySteamId(Handle plugin, int numParams) +{ + if (DB == INVALID_HANDLE) + { + ThrowNativeError(SP_ERROR_NATIVE, "SourceBans++ database is not available."); + return 0; + } + + int admin = GetNativeCell(1); + int iTime = GetNativeCell(4); + + char steamId[MAX_AUTHID_LENGTH], name[MAX_NAME_LENGTH], reason[128]; + GetNativeString(2, steamId, sizeof(steamId)); + GetNativeString(3, name, sizeof(name)); + GetNativeString(5, reason, sizeof(reason)); + + if (strncmp(steamId, "STEAM_", 6, false) != 0) + { + ThrowNativeError(SP_ERROR_NATIVE, "SBPP_BanPlayerBySteamId: steamId must be in SteamID2 format (STEAM_X:Y:Z), got: %s", steamId); + return 0; + } + + if (reason[0] == '\0') + strcopy(reason, sizeof(reason), "Banned by SourceBans"); + + char adminAuth[MAX_AUTHID_LENGTH], adminIp[16]; + if (!admin || !IsClientInGame(admin)) + { + strcopy(adminAuth, sizeof(adminAuth), "STEAM_ID_SERVER"); + strcopy(adminIp, sizeof(adminIp), ServerIp); + } + else + { + strcopy(adminAuth, sizeof(adminAuth), g_sSteamIDs[admin]); + strcopy(adminIp, sizeof(adminIp), g_sPlayerIP[admin]); + } + + DataPack pack = new DataPack(); + pack.WriteCell(admin); + pack.WriteCell(iTime); + pack.WriteString(reason); + pack.WriteString(steamId); + pack.WriteString(name); + pack.WriteString(adminAuth); + pack.WriteString(adminIp); + + char steamIdEscaped[MAX_AUTHID_LENGTH * 2 + 1]; + DB.Escape(steamId, steamIdEscaped, sizeof(steamIdEscaped)); + + char query[512]; + FormatEx(query, sizeof(query), "SELECT bid FROM %s_bans WHERE type = 0 AND authid = '%s' AND (length = 0 OR ends > UNIX_TIMESTAMP()) AND RemoveType IS NULL", + DatabasePrefix, steamIdEscaped); + + DB.Query(DB_OnBanBySteamIdSelect, query, pack, DBPrio_High); + + return 0; +} + +void DB_OnBanBySteamIdSelect(Database db, DBResultSet results, const char[] error, DataPack pack) +{ + if (results == null) + { + LogToFile(logFile, "[SBPP] BanPlayerBySteamId select failed: %s", error); + delete pack; + return; + } + + pack.Reset(); + int admin = pack.ReadCell(); + int iTime = pack.ReadCell(); + char reason[128], steamId[MAX_AUTHID_LENGTH], name[MAX_NAME_LENGTH], adminAuth[MAX_AUTHID_LENGTH], adminIp[16]; + pack.ReadString(reason, sizeof(reason)); + pack.ReadString(steamId, sizeof(steamId)); + pack.ReadString(name, sizeof(name)); + pack.ReadString(adminAuth, sizeof(adminAuth)); + pack.ReadString(adminIp, sizeof(adminIp)); + delete pack; + + if (results.RowCount > 0) + { + LogToFile(logFile, "[SBPP] BanPlayerBySteamId: %s is already banned, skipping.", steamId); + return; + } + + char steamIdEscaped[MAX_AUTHID_LENGTH * 2 + 1], nameEscaped[MAX_NAME_LENGTH * 2 + 1], reasonEscaped[256]; + DB.Escape(steamId, steamIdEscaped, sizeof(steamIdEscaped)); + DB.Escape(name, nameEscaped, sizeof(nameEscaped)); + DB.Escape(reason, reasonEscaped, sizeof(reasonEscaped)); + + char query[1024]; + if (serverID == -1) + { + FormatEx(query, sizeof(query), "INSERT INTO %s_bans (authid, name, created, ends, length, reason, aid, adminIp, admin_name, sid, country) VALUES \ + ('%s', '%s', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + %d, %d, '%s', \ + IFNULL((SELECT aid FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'),'0'), '%s', \ + IFNULL((SELECT user FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'), ''), \ + (SELECT sid FROM %s_servers WHERE ip = '%s' AND port = '%s' LIMIT 0,1), ' ')", + DatabasePrefix, steamIdEscaped, nameEscaped, (iTime * 60), (iTime * 60), reasonEscaped, + DatabasePrefix, adminAuth, adminAuth[8], adminIp, + DatabasePrefix, adminAuth, adminAuth[8], + DatabasePrefix, ServerIp, ServerPort); + } + else + { + FormatEx(query, sizeof(query), "INSERT INTO %s_bans (authid, name, created, ends, length, reason, aid, adminIp, admin_name, sid, country) VALUES \ + ('%s', '%s', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + %d, %d, '%s', \ + IFNULL((SELECT aid FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'),'0'), '%s', \ + IFNULL((SELECT user FROM %s_admins WHERE authid = '%s' OR authid REGEXP '^STEAM_[0-9]:%s$'), ''), \ + %d, ' ')", + DatabasePrefix, steamIdEscaped, nameEscaped, (iTime * 60), (iTime * 60), reasonEscaped, + DatabasePrefix, adminAuth, adminAuth[8], adminIp, + DatabasePrefix, adminAuth, adminAuth[8], + serverID); + } + + DataPack fwdPack = new DataPack(); + fwdPack.WriteCell(admin); + fwdPack.WriteCell(iTime); + fwdPack.WriteString(reason); + + DB.Query(DB_OnBanBySteamIdInsert, query, fwdPack, DBPrio_High); +} + +void DB_OnBanBySteamIdInsert(Database db, DBResultSet results, const char[] error, DataPack pack) +{ + pack.Reset(); + int admin = pack.ReadCell(); + int iTime = pack.ReadCell(); + char reason[128]; + pack.ReadString(reason, sizeof(reason)); + delete pack; + + if (results == null) + { + LogToFile(logFile, "[SBPP] BanPlayerBySteamId insert failed: %s", error); + return; + } + + Call_StartForward(g_hFwd_OnBanAdded); + Call_PushCell(admin); + Call_PushCell(-1); + Call_PushCell(iTime); + Call_PushString(reason); + Call_Finish(); +} + public int Native_SBReportPlayer(Handle plugin, int numParams) { if (numParams < 3) From 99b7e22375c01c9bf206167b46b50274f434a9dd Mon Sep 17 00:00:00 2001 From: Rushaway Date: Thu, 3 Sep 2026 14:13:16 +0200 Subject: [PATCH 2/2] review fixes: harden SBPP_BanPlayerBySteamId Follow-up review pass on the sbpp/sourcebans-pp#1511 port. - Grow the INSERT buffer from 1024 to 2048. The fork's extra admin_name column adds a second IFNULL((SELECT user FROM %s_admins ...)) subquery plus two more adminAuth expansions, pushing the worst-case rendering to ~1210 bytes (128-byte escaped authid, 64-byte escaped name, 256-byte escaped reason, four DatabasePrefix expansions). FormatEx would have silently truncated that into invalid SQL. - Size reasonEscaped as sizeof(reason) * 2 + 1 (257) instead of 256. SQL_EscapeString refuses to write anything when the destination is one byte short, so a 128-char reason of all quotes would have spliced an uninitialised buffer into the INSERT. - Replace the "STEAM_" prefix test with UTIL_IsValidSteamID2(), a full STEAM_X:Y:Z check. The INSERT slices authid[8] to build the '^STEAM_[0-9]:%s$' REGEXP, so "STEAM_junk" previously produced a garbage ban row instead of an error. The helper short-circuits before reading past the terminator on every truncated prefix. - Reject negative iTime, which would otherwise store a negative length and an ends timestamp in the past. - Bounds-check iAdmin before IsClientInGame()/g_sSteamIDs[] and require the ban flag, mirroring Native_SBBanPlayer. An out-of-range index previously faulted the native. - Normalise a non-live iAdmin to 0 so SBPP_OnBanPlayer subscribers never receive an unusable client index. - Use the callback's db handle rather than the global DB, make both callbacks public, and drop the "[SBPP] " log prefix, matching SelectAddbanCallback / InsertAddbanCallback. - Document the new error conditions on the native and the iTarget = -1 contract on the SBPP_OnBanPlayer forward. Verified with spcomp 1.12.0.7253: sbpp_main, sbpp_sleuth and sbpp_report all compile clean with no warnings. Co-Authored-By: Claude Sonnet 5 Co-authored-by: Sigibert --- .../scripting/include/sourcebanspp.inc | 18 +++- game/addons/sourcemod/scripting/sbpp_main.sp | 92 ++++++++++++++++--- 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/game/addons/sourcemod/scripting/include/sourcebanspp.inc b/game/addons/sourcemod/scripting/include/sourcebanspp.inc index 4f534b7c2..7c28d898c 100644 --- a/game/addons/sourcemod/scripting/include/sourcebanspp.inc +++ b/game/addons/sourcemod/scripting/include/sourcebanspp.inc @@ -115,11 +115,17 @@ native void SBPP_BanPlayer(int iAdmin, int iTarget, int iTime, const char[] sRea * Inserts the ban directly into the SourceBans++ database. * Fires SBPP_OnBanPlayer with iTarget = -1 to indicate an offline ban. * - * @param iAdmin Client index of the admin (0 = server / automated) - * @param steamId SteamID string, e.g. "STEAM_0:0:12345" + * @param iAdmin Client index of the admin (0 = server / automated). A + * non-zero index must be an in-game client holding the ban + * flag; anything else errors. Indexes that are not live + * clients are reported as 0 to SBPP_OnBanPlayer. + * @param steamId SteamID2 string, e.g. "STEAM_0:0:12345". Malformed input + * raises a native error rather than inserting a bad row. * @param name Player name; may be empty string if unknown - * @param iTime Ban duration in minutes (0 = permanent) - * @param sReason Reason for the ban + * @param iTime Ban duration in minutes (0 = permanent, must be >= 0) + * @param sReason Reason for the ban; empty falls back to "Banned by SourceBans" + * @error Database unavailable, malformed steamId, negative iTime, or + * iAdmin is a client without ban privileges. * @noreturn *********************************************************/ native void SBPP_BanPlayerBySteamId(int iAdmin, const char[] steamId, const char[] name, int iTime, const char[] sReason); @@ -138,7 +144,9 @@ native void SBPP_ReportPlayer(int iReporter, int iTarget, const char[] sReason); * Called when the admin banning the player. * * @param iAdmin The client index of the admin who is banning the client - * @param iTarget The client index of the player to ban + * @param iTarget The client index of the player to ban, or -1 when the ban + * came from SBPP_BanPlayerBySteamId() and the target is not + * on the server. Always range-check before using it. * @param iTime The time to ban the player for (in minutes, 0 = permanent) * @param sReason The reason to ban the player from the server *********************************************************/ diff --git a/game/addons/sourcemod/scripting/sbpp_main.sp b/game/addons/sourcemod/scripting/sbpp_main.sp index 4febb5819..6e87523d0 100644 --- a/game/addons/sourcemod/scripting/sbpp_main.sp +++ b/game/addons/sourcemod/scripting/sbpp_main.sp @@ -2387,6 +2387,33 @@ public int Native_SBBanPlayer(Handle plugin, int numParams) return true; } +// Validates a SteamID2 authid ("STEAM_X:Y:Z" with X/Y single digits and Z a +// non-empty run of digits). A prefix-only check would let malformed strings +// such as "STEAM_junk" through, and the INSERT below slices authid[8] to build +// the '^STEAM_[0-9]:%s$' REGEXP, so a bad tail silently produces a garbage row. +static bool UTIL_IsValidSteamID2(const char[] authid) +{ + if (strncmp(authid, "STEAM_", 6, false) != 0) + return false; + + if (!IsCharNumeric(authid[6]) || authid[7] != ':') + return false; + + if ((authid[8] != '0' && authid[8] != '1') || authid[9] != ':') + return false; + + if (authid[10] == '\0') + return false; + + for (int i = 10; authid[i] != '\0'; i++) + { + if (!IsCharNumeric(authid[i])) + return false; + } + + return true; +} + public int Native_SBPP_BanPlayerBySteamId(Handle plugin, int numParams) { if (DB == INVALID_HANDLE) @@ -2403,18 +2430,47 @@ public int Native_SBPP_BanPlayerBySteamId(Handle plugin, int numParams) GetNativeString(3, name, sizeof(name)); GetNativeString(5, reason, sizeof(reason)); - if (strncmp(steamId, "STEAM_", 6, false) != 0) + if (!UTIL_IsValidSteamID2(steamId)) { ThrowNativeError(SP_ERROR_NATIVE, "SBPP_BanPlayerBySteamId: steamId must be in SteamID2 format (STEAM_X:Y:Z), got: %s", steamId); return 0; } + if (iTime < 0) + { + ThrowNativeError(SP_ERROR_NATIVE, "SBPP_BanPlayerBySteamId: iTime must be >= 0 (0 = permanent), got: %d", iTime); + return 0; + } + + // Mirrors Native_SBBanPlayer: a client index is only honoured when it maps + // to a real, in-game admin holding the ban flag. Bounds-check first so a + // bogus index cannot fault IsClientInGame()/g_sSteamIDs[]. + bool bHasAdmin = (admin > 0 && admin <= MaxClients && IsClientInGame(admin)); + if (bHasAdmin) + { + AdminId aid = GetUserAdmin(admin); + if (aid == INVALID_ADMIN_ID) + { + ThrowNativeError(SP_ERROR_NATIVE, "Ban Error: Player is not an admin."); + return 0; + } + + if (!aid.HasFlag(Admin_Ban)) + { + ThrowNativeError(SP_ERROR_NATIVE, "Ban Error: Player does not have BAN flag."); + return 0; + } + } + if (reason[0] == '\0') strcopy(reason, sizeof(reason), "Banned by SourceBans"); char adminAuth[MAX_AUTHID_LENGTH], adminIp[16]; - if (!admin || !IsClientInGame(admin)) + if (!bHasAdmin) { + // Collapse anything that is not a live client to 0 (server/automated) so + // SBPP_OnBanPlayer never hands subscribers an index they cannot use. + admin = 0; strcopy(adminAuth, sizeof(adminAuth), "STEAM_ID_SERVER"); strcopy(adminIp, sizeof(adminIp), ServerIp); } @@ -2445,11 +2501,11 @@ public int Native_SBPP_BanPlayerBySteamId(Handle plugin, int numParams) return 0; } -void DB_OnBanBySteamIdSelect(Database db, DBResultSet results, const char[] error, DataPack pack) +public void DB_OnBanBySteamIdSelect(Database db, DBResultSet results, const char[] error, DataPack pack) { if (results == null) { - LogToFile(logFile, "[SBPP] BanPlayerBySteamId select failed: %s", error); + LogToFile(logFile, "BanPlayerBySteamId Select Query Failed: %s", error); delete pack; return; } @@ -2467,16 +2523,24 @@ void DB_OnBanBySteamIdSelect(Database db, DBResultSet results, const char[] erro if (results.RowCount > 0) { - LogToFile(logFile, "[SBPP] BanPlayerBySteamId: %s is already banned, skipping.", steamId); + LogToFile(logFile, "BanPlayerBySteamId: %s is already banned, skipping.", steamId); return; } - char steamIdEscaped[MAX_AUTHID_LENGTH * 2 + 1], nameEscaped[MAX_NAME_LENGTH * 2 + 1], reasonEscaped[256]; - DB.Escape(steamId, steamIdEscaped, sizeof(steamIdEscaped)); - DB.Escape(name, nameEscaped, sizeof(nameEscaped)); - DB.Escape(reason, reasonEscaped, sizeof(reasonEscaped)); - - char query[1024]; + // reasonEscaped must hold sizeof(reason) * 2 + 1: SQL_EscapeString() refuses + // to write at all when the destination is one byte short, which would leave + // an uninitialised buffer spliced into the INSERT below. + char steamIdEscaped[MAX_AUTHID_LENGTH * 2 + 1], nameEscaped[MAX_NAME_LENGTH * 2 + 1], reasonEscaped[sizeof(reason) * 2 + 1]; + db.Escape(steamId, steamIdEscaped, sizeof(steamIdEscaped)); + db.Escape(name, nameEscaped, sizeof(nameEscaped)); + db.Escape(reason, reasonEscaped, sizeof(reasonEscaped)); + + // 2048, not 1024: with the fork's extra admin_name sub-select the worst-case + // rendering of this statement is ~1210 bytes (128-byte escaped authid, + // 64-byte escaped name, 256-byte escaped reason, four DatabasePrefix and + // four adminAuth expansions), which FormatEx would silently truncate into + // invalid SQL. + char query[2048]; if (serverID == -1) { FormatEx(query, sizeof(query), "INSERT INTO %s_bans (authid, name, created, ends, length, reason, aid, adminIp, admin_name, sid, country) VALUES \ @@ -2507,10 +2571,10 @@ void DB_OnBanBySteamIdSelect(Database db, DBResultSet results, const char[] erro fwdPack.WriteCell(iTime); fwdPack.WriteString(reason); - DB.Query(DB_OnBanBySteamIdInsert, query, fwdPack, DBPrio_High); + db.Query(DB_OnBanBySteamIdInsert, query, fwdPack, DBPrio_High); } -void DB_OnBanBySteamIdInsert(Database db, DBResultSet results, const char[] error, DataPack pack) +public void DB_OnBanBySteamIdInsert(Database db, DBResultSet results, const char[] error, DataPack pack) { pack.Reset(); int admin = pack.ReadCell(); @@ -2521,7 +2585,7 @@ void DB_OnBanBySteamIdInsert(Database db, DBResultSet results, const char[] erro if (results == null) { - LogToFile(logFile, "[SBPP] BanPlayerBySteamId insert failed: %s", error); + LogToFile(logFile, "BanPlayerBySteamId Insert Query Failed: %s", error); return; }