Skip to content
Merged
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
217 changes: 211 additions & 6 deletions addons/sourcemod/scripting/FixSprayExploit.sp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@



#define PLUGIN_VERSION "2.26"
#define PLUGIN_VERSION "2.28"

/*=======================================================================================
Plugin Info:
Expand All @@ -32,6 +32,18 @@
========================================================================================
Change Log:

2.28 (02-Dec-2025)
- Added file size validation to detect malformed sprays by comparing header size with actual file size.
- Extended g_iVal array to cover offset 62 for better VTF header validation.
- Added VTF format constants and helper functions (GetFormatInfo, CalcSize, MaxValC).
- Added 5% tolerance threshold for file size comparison to minimize false positives.
- Enhanced logging to include size mismatch details (actual size, expected size, difference percentage).
- Forward "OnSprayExploit" now uses special code (-2) for size mismatch errors.
- Thanks to null138 for reporting and testing.

2.27 (24-Jun-2025)
- Added a check for the latest spray exploit. Thanks to ".Rushaway" for fixing and reporting.

2.26 (21-May-2025)
- Added native "SprayExploitFixer_LogCustom" to log custom messages. Requested by ".Rushaway".
- Added RegPluginLibrary "spray_exploit_fixer".
Expand Down Expand Up @@ -195,7 +207,26 @@
#define TIMEOUT_LOG 10.0
#define PATH_BACKUP "backup_sprays"

int g_iVal[] = {86,84,70,0,7,0,0,0,42,0,0,0,42,0,0,0,42,42,42,42,42,42,42,42,42,42,42,0,0,0,0,0,0,0,0,0};
// VTF formats
#define FVTF_RGBA8888 0
#define FVTF_ABGR8888 1
#define FVTF_RGB888 2
#define FVTF_BGR888 3
#define FVTF_RGB565 4
#define FVTF_I8 5
#define FVTF_IA88 6
#define FVTF_DXT1 7
#define FVTF_DXT5 9
#define FVTF_BGRA8888 12
#define FVTF_DXT1_ALT 13
#define FVTF_DXT3 14
#define FVTF_DXT5_ALT 15
#define FVTF_BGRX8888 16

Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The 5% size tolerance threshold (SIZE_TOLERANCE = 0.05) is defined but lacks explanation for why this specific value was chosen. Consider adding a comment explaining the rationale (e.g., "allows for file format overhead, compression variations, or minor header extensions") to help future maintainers understand if this value needs adjustment based on false positive rates.

Suggested change
// Tolerance for file size mismatch when validating spray files.
// 5% allows for file format overhead, compression variations, or minor header extensions.
// Adjust this value if false positives/negatives are observed in spray validation.

Copilot uses AI. Check for mistakes.
#define SIZE_TOLERANCE 0.05 // 5% tolerance

int g_iVal[] = {86,84,70,0,7,0,0,0,42,0,0,0,42,0,0,0,42,42,42,42,42,42,42,42,42,42,42,0,0,0,0,0,0,0,0,0,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42};
bool g_bIsVTFExploit[MAXPLAYERS+1] = {false, ...};
char g_sFilename[PLATFORM_MAX_PATH];
char g_sMoveFiles[PLATFORM_MAX_PATH];
char g_sDownloads[PLATFORM_MAX_PATH];
Expand Down Expand Up @@ -355,6 +386,7 @@ public void OnClientPutInServer(int client)

public void OnClientConnected(int client)
{
g_bIsVTFExploit[client] = false;
g_fSprayed[client] = 0.0;
g_sPath1[client][0] = 0;
g_sPath2[client][0] = 0;
Expand All @@ -373,6 +405,7 @@ public void OnClientDisconnect(int client)
g_sAuth[client][0] = 0;
g_sAuth[client][6] = 0;
g_sAuthUnverified[client][0] = 0;
g_bIsVTFExploit[client] = false;

/*
static char sPath[PLATFORM_MAX_PATH];
Expand Down Expand Up @@ -797,6 +830,7 @@ Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, fl
g_fSprayed[client] = GetGameTime();
if( g_hCvarLog.IntValue ) LogCustom("Blocked invalid spray: %s from (%N) [%s]", g_sFilename, client, auth);
if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Blocked invalid spray: %s from (%N) [%s]", g_sFilename, client, auth);
g_bIsVTFExploit[client] = true;
}

if( g_hCvarPunish.IntValue == 1 || g_hCvarPunish.IntValue >= 3)
Expand All @@ -811,6 +845,7 @@ Action PlayerDecal(const char[] te_name, const int[] Players, int numClients, fl

if( g_hCvarLog.IntValue ) LogCustom("Blocked unchecked spray - missing file: %s from (%N) [%s]", g_sFilename, client, auth);
if( g_hCvarMsg.IntValue == 1 ) PrintToServer("[Spray Exploit] Blocked unchecked spray - missing file: %s from (%N) [%s]", g_sFilename, client, auth);
g_bIsVTFExploit[client] = true;
}
}

Expand All @@ -832,7 +867,7 @@ void ReqTempEnt(DataPack hPack)

int client = hPack.ReadCell();
client = GetClientOfUserId(client);
if( client )
if( client && !g_bIsVTFExploit[client] )
{
float vPos[3];
vPos[0] = hPack.ReadFloat();
Expand Down Expand Up @@ -1003,6 +1038,10 @@ void FileCheck()
if( hFile )
{
hFile.Read(iRead, sizeof(iRead), 1);

// Get actual file size
hFile.Seek(0, SEEK_END);
int actualSize = hFile.Position;
delete hFile;

int i = ValFile(iRead);
Expand All @@ -1026,6 +1065,8 @@ void FileCheck()
if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray: %s: %02d (%02X <> %02X)", g_sFilename, i, iRead[i], g_iVal[i]);
}

g_bIsVTFExploit[client] = true;
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting g_bIsVTFExploit[client] when client is 0 will corrupt memory by writing to index 0 of the array (which represents an invalid client). This can occur when GetClientFromSpray() and GetClientFromJingle() both return 0. Add a check: if (client) g_bIsVTFExploit[client] = true; to prevent this.

Copilot uses AI. Check for mistakes.

Call_StartForward(g_hExploit);
Call_PushCell(client);
Call_PushCell(i);
Expand All @@ -1039,6 +1080,46 @@ void FileCheck()
return;
}

// Check file size
int calculatedSize = CalcSize(iRead);
if( calculatedSize > 0 )
{
float sizeDiff = FloatAbs(float(actualSize - calculatedSize)) / float(calculatedSize);
if( sizeDiff > SIZE_TOLERANCE )
{
int client = GetClientFromSpray();
if( !client ) client = GetClientFromJingle();
if( client )
{
static char auth[64];
if ( g_sAuth[client][6] == 'I' )
FormatEx(auth, sizeof(auth), "Unverified: %s", g_sAuthUnverified[client]);
else
FormatEx(auth, sizeof(auth), "%s", g_sAuth[client]);

if( g_hCvarLog.IntValue ) LogCustom("Invalid spray (size mismatch): %s from (%N) [%s] - Actual: %d, Expected: %d, Diff: %.1f%%", g_sFilename, client, auth, actualSize, calculatedSize, sizeDiff * 100.0);
if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray (size mismatch): %s from (%N) [%s] - Actual: %d, Expected: %d, Diff: %.1f%%", g_sFilename, client, auth, actualSize, calculatedSize, sizeDiff * 100.0);
} else {
if( g_hCvarLog.IntValue ) LogCustom("Invalid spray (size mismatch): %s - Actual: %d, Expected: %d, Diff: %.1f%%", g_sFilename, actualSize, calculatedSize, sizeDiff * 100.0);
if( g_hCvarMsg.IntValue ) PrintToServer("[Spray Exploit] Invalid spray (size mismatch): %s - Actual: %d, Expected: %d, Diff: %.1f%%", g_sFilename, actualSize, calculatedSize, sizeDiff * 100.0);
}

g_bIsVTFExploit[client] = true;
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue: Setting g_bIsVTFExploit[client] when client is 0 will corrupt memory. Add a check: if (client) g_bIsVTFExploit[client] = true; to prevent writing to an invalid array index.

Suggested change
g_bIsVTFExploit[client] = true;
if (client)
g_bIsVTFExploit[client] = true;

Copilot uses AI. Check for mistakes.

Call_StartForward(g_hExploit);
Call_PushCell(client);
Call_PushCell(-2); // Special code for size mismatch
Call_PushCell(actualSize);
Call_Finish();

if( g_hCvarPunish.IntValue >= 2 )
TestClient(client);

g_smChecked.SetValue(g_sFilename, false);
return;
}
}

g_smChecked.SetValue(g_sFilename, true);
} else {
if( g_hCvarLog.IntValue ) LogCustom("Missing file: %s", g_sFilename);
Expand All @@ -1057,7 +1138,8 @@ int ValFile(int iRead[sizeof(g_iVal)])
return -1;
}

if( iRead[21] == 42 && iRead[24] > 1 || iRead[16] == 80 && iRead[24] > 1)
// 66 frames is more than enough?
if ((iRead[24] | (iRead[25] << 8)) > 66)
{
return 24;
}
Expand All @@ -1072,7 +1154,7 @@ int ValFile(int iRead[sizeof(g_iVal)])
{
switch( i )
{
case 8: read = iRead[i] <= 5;
case 8: read = iRead[i] <= 5;
case 16, 18:
{
FormatEx(bytes, sizeof(bytes), "%02X%02X", iRead[i+1], iRead[i]);
Expand All @@ -1083,7 +1165,7 @@ int ValFile(int iRead[sizeof(g_iVal)])
{
FormatEx(bytes, sizeof(bytes), "%02X%02X%02X%02X", iRead[i+3], iRead[i+2], iRead[i+1], iRead[i]);
n = HexToDec(bytes);
if( n & (0x8000|0x10000|0x800000) ) read = false;
if( n & (0x8000|0x10000|0x80000|0x800000) ) read = false; // added pre_srgb check
}
/*
case 25:
Expand Down Expand Up @@ -1128,6 +1210,129 @@ int HexToDec(char[] bytes)
return value;
}

stock int MaxValC(int a, int b)
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stock keyword is deprecated in modern SourcePawn with #pragma newdecls required. This function should be declared as static int MaxValC(int a, int b) instead to follow current SourcePawn best practices.

Suggested change
stock int MaxValC(int a, int b)
static int MaxValC(int a, int b)

Copilot uses AI. Check for mistakes.
{
return (a > b) ? a : b;
}

bool GetFormatInfo(int fmt, bool &compressed, int &bpp)
{
switch(fmt)
{
case FVTF_RGBA8888, FVTF_ABGR8888, FVTF_BGRA8888, FVTF_BGRX8888:
{
bpp = 32;
compressed = false;
return true;
}

case FVTF_RGB888, FVTF_BGR888:
{
bpp = 24;
compressed = false;
return true;
}

case FVTF_RGB565, FVTF_IA88:
{
bpp = 16;
compressed = false;
return true;
}

case FVTF_I8:
{
bpp = 8;
compressed = false;
return true;
}

case FVTF_DXT1, FVTF_DXT1_ALT:
{
bpp = 8;
compressed = true;
return true;
}

case FVTF_DXT3, FVTF_DXT5, FVTF_DXT5_ALT:
{
bpp = 16;
compressed = true;
return true;
}

default:
{
bpp = 32;
compressed = false;
return true;
Comment on lines +1266 to +1268
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VTF format constant definitions have gaps (e.g., format 8, 10-11 are missing between DXT1=7, DXT5=9, and BGRA8888=12). While this may match the VTF specification, GetFormatInfo defaults unknown formats to 32bpp uncompressed (line 1266), which could lead to incorrect size calculations for these formats. Consider explicitly handling or documenting which formats are intentionally unsupported to avoid false positives or security bypasses.

Suggested change
bpp = 32;
compressed = false;
return true;
// Unknown/unsupported format. Do not accept as valid.
bpp = 0;
compressed = false;
return false;

Copilot uses AI. Check for mistakes.
}
}
}
Comment on lines +1218 to +1271
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new function GetFormatInfo lacks documentation explaining its purpose, parameters, and return value. Consider adding a docstring that explains: 1) Maps VTF format codes to compression type and bits-per-pixel, 2) Returns false if format is unrecognized (though current implementation always returns true), 3) Uses output parameters for compressed and bpp.

Copilot uses AI. Check for mistakes.

int CalcSize(int iRead[sizeof(g_iVal)])
{
int versionMajor = iRead[4] | (iRead[5]<<8) | (iRead[6]<<16) | (iRead[7]<<24);
int versionMinor = iRead[8] | (iRead[9]<<8) | (iRead[10]<<16) | (iRead[11]<<24);

if(versionMajor != 7 || versionMinor > 6)
{
return -1;
}

int width = iRead[16] | (iRead[17]<<8);
int height = iRead[18] | (iRead[19]<<8);
int frames = MaxValC(1, iRead[24] | (iRead[25]<<8));
int numMip = iRead[56];

int highresFmt = iRead[52] | (iRead[53]<<8) | (iRead[54]<<16) | (iRead[55]<<24);
int lowresFmt = iRead[57];

int lowresW = iRead[61];
int lowresH = iRead[62];

bool compressed; int bpp;
if(!GetFormatInfo(highresFmt, compressed, bpp))
return -1;

int highresSize = 0;
for(int mip=0; mip<numMip; mip++)
{
int mw = MaxValC(1, width>>mip);
int mh = MaxValC(1, height>>mip);
if(compressed)
{
int bw = (mw+3)/4;
int bh = (mh+3)/4;
highresSize += bw*bh*bpp;
}
else
{
highresSize += mw*mh*bpp/8;
}
}
highresSize *= frames;
Comment on lines +1298 to +1314
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential integer overflow in size calculations. When calculating highresSize += bw*bh*bpp (line 1307) or highresSize += mw*mh*bpp/8 (line 1311), large dimensions could cause integer overflow before the result is added to highresSize. Similarly, multiplying by frames (line 1314) could overflow. Consider using 64-bit integers or adding overflow checks to prevent incorrect size validation that could allow malicious files to bypass detection.

Copilot uses AI. Check for mistakes.

// thumbnail
if(!GetFormatInfo(lowresFmt, compressed, bpp))
return highresSize; // sometimes this might happen
int thumbSize = 0;
int mw = lowresW;
int mh = lowresH;
if(compressed)
{
int bw = (mw+3)/4;
int bh = (mh+3)/4;
thumbSize = bw*bh*bpp;
}
else
{
thumbSize = mw*mh*bpp/8;
}

return highresSize + thumbSize;
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CalcSize function calculates only the image data size (mipmaps + thumbnail) but doesn't account for the VTF file header. VTF files have an 80-byte header (version 7.0+), so the actual file size should be headerSize + highresSize + thumbSize. This will cause false positives where valid spray files are flagged as having size mismatches. Consider returning 80 + highresSize + thumbSize instead.

Suggested change
return highresSize + thumbSize;
return 80 + highresSize + thumbSize;

Copilot uses AI. Check for mistakes.
}
Comment on lines +1273 to +1334
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new function CalcSize lacks documentation explaining its purpose, parameters, and return value. Consider adding a docstring that explains: 1) It calculates the expected VTF file size based on header data, 2) Returns -1 on invalid format/version, 3) Returns calculated size in bytes for valid files. This is especially important given the complexity of the VTF format calculations.

Copilot uses AI. Check for mistakes.

void LogCustom(const char[] format, any ...)
{
static char buffer[512];
Expand Down