|
31 | 31 | #include "Common/CRCDebug.h" |
32 | 32 | #include "Common/Debug.h" |
33 | 33 | #include "Common/file.h" |
| 34 | +#include "Common/FileSystem.h" |
34 | 35 | #include "Common/GameAudio.h" |
35 | 36 | #include "Common/LocalFileSystem.h" |
36 | 37 | #include "Common/Player.h" |
|
52 | 53 | #include "GameLogic/VictoryConditions.h" |
53 | 54 | #include "GameClient/DisconnectMenu.h" |
54 | 55 | #include "GameClient/InGameUI.h" |
| 56 | +#include "TARGA.h" |
55 | 57 |
|
56 | 58 | static Bool hasValidTransferFileExtension(const AsciiString& filePath) |
57 | 59 | { |
@@ -84,6 +86,125 @@ static Bool hasValidTransferFileExtension(const AsciiString& filePath) |
84 | 86 | return false; |
85 | 87 | } |
86 | 88 |
|
| 89 | +enum TransferFileType |
| 90 | +{ |
| 91 | + TransferFileType_Invalid = -1, |
| 92 | + TransferFileType_Map, |
| 93 | + TransferFileType_Ini, |
| 94 | + TransferFileType_Str, |
| 95 | + TransferFileType_Txt, |
| 96 | + TransferFileType_Tga, |
| 97 | + TransferFileType_Wak, |
| 98 | + TransferFileType_Count |
| 99 | +}; |
| 100 | + |
| 101 | +struct TransferFileRule |
| 102 | +{ |
| 103 | + const char* ext; |
| 104 | + UnsignedInt maxSize; |
| 105 | +}; |
| 106 | + |
| 107 | +static const TransferFileRule transferFileRules[TransferFileType_Count] = |
| 108 | +{ |
| 109 | + { ".map", 5 * 1024 * 1024 }, |
| 110 | + { ".ini", 2 * 1024 * 1024 }, |
| 111 | + { ".str", 512 * 1024 }, |
| 112 | + { ".txt", 1 * 1024 * 1024 }, |
| 113 | + { ".tga", 2 * 1024 * 1024 }, |
| 114 | + { ".wak", 128 * 1024 }, |
| 115 | +}; |
| 116 | + |
| 117 | +static TransferFileType getTransferFileType(const char* extension) |
| 118 | +{ |
| 119 | + for (Int i = 0; i < TransferFileType_Count; ++i) |
| 120 | + { |
| 121 | + if (stricmp(extension, transferFileRules[i].ext) == 0) |
| 122 | + { |
| 123 | + return static_cast<TransferFileType>(i); |
| 124 | + } |
| 125 | + } |
| 126 | + return TransferFileType_Invalid; |
| 127 | +} |
| 128 | + |
| 129 | +static Bool hasValidTransferFileContent(const AsciiString& filePath, const UnsignedByte* data, UnsignedInt dataSize) |
| 130 | +{ |
| 131 | + const char* fileExt = strrchr(filePath.str(), '.'); |
| 132 | + if (fileExt == nullptr) |
| 133 | + { |
| 134 | + DEBUG_LOG(("File '%s' has no extension for content validation.", filePath.str())); |
| 135 | + return false; |
| 136 | + } |
| 137 | + |
| 138 | + const TransferFileType fileType = getTransferFileType(fileExt); |
| 139 | + if (fileType == TransferFileType_Invalid) |
| 140 | + { |
| 141 | + DEBUG_LOG(("File '%s' has unrecognized extension '%s' for content validation.", filePath.str(), fileExt)); |
| 142 | + return false; |
| 143 | + } |
| 144 | + |
| 145 | + // Check size limit |
| 146 | + const TransferFileRule& rule = transferFileRules[fileType]; |
| 147 | + if (dataSize > rule.maxSize) |
| 148 | + { |
| 149 | + DEBUG_LOG(("File '%s' exceeds maximum size (%u bytes, limit %u bytes).", filePath.str(), dataSize, rule.maxSize)); |
| 150 | + return false; |
| 151 | + } |
| 152 | + |
| 153 | + // Extension-specific content validation |
| 154 | + switch (fileType) |
| 155 | + { |
| 156 | + case TransferFileType_Map: |
| 157 | + { |
| 158 | + if (dataSize < 4 || memcmp(data, "CkMp", 4) != 0) |
| 159 | + { |
| 160 | + DEBUG_LOG(("Map file '%s' has invalid magic bytes.", filePath.str())); |
| 161 | + return false; |
| 162 | + } |
| 163 | + break; |
| 164 | + } |
| 165 | + |
| 166 | + case TransferFileType_Ini: |
| 167 | + { |
| 168 | + for (UnsignedInt i = 0; i < dataSize; ++i) |
| 169 | + { |
| 170 | + if (data[i] == 0) |
| 171 | + { |
| 172 | + DEBUG_LOG(("INI file '%s' contains null bytes (likely binary).", filePath.str())); |
| 173 | + return false; |
| 174 | + } |
| 175 | + } |
| 176 | + break; |
| 177 | + } |
| 178 | + |
| 179 | + case TransferFileType_Tga: |
| 180 | + { |
| 181 | + if (dataSize < sizeof(TGAHeader) + sizeof(TGA2Footer)) |
| 182 | + { |
| 183 | + DEBUG_LOG(("TGA file '%s' is too small to be valid.", filePath.str())); |
| 184 | + return false; |
| 185 | + } |
| 186 | + TGA2Footer footer; |
| 187 | + memcpy(&footer, data + dataSize - sizeof(footer), sizeof(footer)); |
| 188 | + const Bool isTGA2 = memcmp(footer.Signature, TGA2_SIGNATURE, sizeof(footer.Signature)) == 0 |
| 189 | + && footer.RsvdChar == '.' |
| 190 | + && footer.BZST == '\0'; |
| 191 | + if (!isTGA2) |
| 192 | + { |
| 193 | + DEBUG_LOG(("TGA file '%s' is missing TRUEVISION-XFILE footer signature.", filePath.str())); |
| 194 | + return false; |
| 195 | + } |
| 196 | + break; |
| 197 | + } |
| 198 | + |
| 199 | + default: |
| 200 | + { |
| 201 | + break; |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + return true; |
| 206 | +} |
| 207 | + |
87 | 208 | /** |
88 | 209 | * Le destructor. |
89 | 210 | */ |
@@ -742,6 +863,20 @@ void ConnectionManager::processFile(NetFileCommandMsg *msg) |
742 | 863 | } |
743 | 864 | #endif // COMPRESS_TARGAS |
744 | 865 |
|
| 866 | + // TheSuperHackers @security bobtista 12/02/2026 Validate file content in memory before writing to disk |
| 867 | + if (!hasValidTransferFileContent(realFileName, buf, len)) |
| 868 | + { |
| 869 | + DEBUG_LOG(("File '%s' failed content validation. Transfer aborted.", realFileName.str())); |
| 870 | +#ifdef COMPRESS_TARGAS |
| 871 | + if (deleteBuf) |
| 872 | + { |
| 873 | + delete[] buf; |
| 874 | + buf = nullptr; |
| 875 | + } |
| 876 | +#endif // COMPRESS_TARGAS |
| 877 | + return; |
| 878 | + } |
| 879 | + |
745 | 880 | File *fp = TheFileSystem->openFile(realFileName.str(), File::CREATE | File::BINARY | File::WRITE); |
746 | 881 | if (fp) |
747 | 882 | { |
|
0 commit comments