diff --git a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp index 66b4e0f2b40..7eba875398c 100644 --- a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -31,6 +31,7 @@ #include "Common/CRCDebug.h" #include "Common/Debug.h" #include "Common/file.h" +#include "Common/FileSystem.h" #include "Common/GameAudio.h" #include "Common/LocalFileSystem.h" #include "Common/Player.h" @@ -52,6 +53,7 @@ #include "GameLogic/VictoryConditions.h" #include "GameClient/DisconnectMenu.h" #include "GameClient/InGameUI.h" +#include "TARGA.h" static Bool hasValidTransferFileExtension(const AsciiString& filePath) { @@ -84,6 +86,125 @@ static Bool hasValidTransferFileExtension(const AsciiString& filePath) return false; } +enum TransferFileType +{ + TransferFileType_Invalid = -1, + TransferFileType_Map, + TransferFileType_Ini, + TransferFileType_Str, + TransferFileType_Txt, + TransferFileType_Tga, + TransferFileType_Wak, + TransferFileType_Count +}; + +struct TransferFileRule +{ + const char* ext; + UnsignedInt maxSize; +}; + +static const TransferFileRule transferFileRules[TransferFileType_Count] = +{ + { ".map", 5 * 1024 * 1024 }, + { ".ini", 512 * 1024 }, + { ".str", 512 * 1024 }, + { ".txt", 512 * 1024 }, + { ".tga", 2 * 1024 * 1024 }, + { ".wak", 512 * 1024 }, +}; + +static TransferFileType getTransferFileType(const char* extension) +{ + for (Int i = 0; i < TransferFileType_Count; ++i) + { + if (stricmp(extension, transferFileRules[i].ext) == 0) + { + return static_cast(i); + } + } + return TransferFileType_Invalid; +} + +static Bool hasValidTransferFileContent(const AsciiString& filePath, const UnsignedByte* data, UnsignedInt dataSize) +{ + const char* fileExt = strrchr(filePath.str(), '.'); + if (fileExt == nullptr) + { + DEBUG_LOG(("File '%s' has no extension for content validation.", filePath.str())); + return false; + } + + const TransferFileType fileType = getTransferFileType(fileExt); + if (fileType == TransferFileType_Invalid) + { + DEBUG_LOG(("File '%s' has unrecognized extension '%s' for content validation.", filePath.str(), fileExt)); + return false; + } + + // Check size limit + const TransferFileRule& rule = transferFileRules[fileType]; + if (dataSize > rule.maxSize) + { + DEBUG_LOG(("File '%s' exceeds maximum size (%u bytes, limit %u bytes).", filePath.str(), dataSize, rule.maxSize)); + return false; + } + + // Extension-specific content validation + switch (fileType) + { + case TransferFileType_Map: + { + if (dataSize < 4 || memcmp(data, "CkMp", 4) != 0) + { + DEBUG_LOG(("Map file '%s' has invalid magic bytes.", filePath.str())); + return false; + } + break; + } + + case TransferFileType_Ini: + { + for (UnsignedInt i = 0; i < dataSize; ++i) + { + if (data[i] == 0) + { + DEBUG_LOG(("INI file '%s' contains null bytes (likely binary).", filePath.str())); + return false; + } + } + break; + } + + case TransferFileType_Tga: + { + if (dataSize < sizeof(TGAHeader) + sizeof(TGA2Footer)) + { + DEBUG_LOG(("TGA file '%s' is too small to be valid.", filePath.str())); + return false; + } + TGA2Footer footer; + memcpy(&footer, data + dataSize - sizeof(footer), sizeof(footer)); + const Bool isTGA2 = memcmp(footer.Signature, TGA2_SIGNATURE, sizeof(footer.Signature)) == 0 + && footer.RsvdChar == '.' + && footer.BZST == '\0'; + if (!isTGA2) + { + DEBUG_LOG(("TGA file '%s' is missing TRUEVISION-XFILE footer signature.", filePath.str())); + return false; + } + break; + } + + default: + { + break; + } + } + + return true; +} + /** * Le destructor. */ @@ -734,6 +855,20 @@ void ConnectionManager::processFile(NetFileCommandMsg *msg) } #endif // COMPRESS_TARGAS + // TheSuperHackers @security bobtista 12/02/2026 Validate file content in memory before writing to disk + if (!hasValidTransferFileContent(realFileName, buf, len)) + { + DEBUG_LOG(("File '%s' failed content validation. Transfer aborted.", realFileName.str())); +#ifdef COMPRESS_TARGAS + if (deleteBuf) + { + delete[] buf; + buf = nullptr; + } +#endif // COMPRESS_TARGAS + return; + } + File *fp = TheFileSystem->openFile(realFileName.str(), File::CREATE | File::BINARY | File::WRITE); if (fp) {