Skip to content

Commit d2518fa

Browse files
authored
fix(connection): Implement file content validation for map transfers (TheSuperHackers#1819)
1 parent 8f263dd commit d2518fa

File tree

1 file changed

+135
-0
lines changed

1 file changed

+135
-0
lines changed

Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "Common/CRCDebug.h"
3232
#include "Common/Debug.h"
3333
#include "Common/file.h"
34+
#include "Common/FileSystem.h"
3435
#include "Common/GameAudio.h"
3536
#include "Common/LocalFileSystem.h"
3637
#include "Common/Player.h"
@@ -52,6 +53,7 @@
5253
#include "GameLogic/VictoryConditions.h"
5354
#include "GameClient/DisconnectMenu.h"
5455
#include "GameClient/InGameUI.h"
56+
#include "TARGA.h"
5557

5658
static Bool hasValidTransferFileExtension(const AsciiString& filePath)
5759
{
@@ -84,6 +86,125 @@ static Bool hasValidTransferFileExtension(const AsciiString& filePath)
8486
return false;
8587
}
8688

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+
87208
/**
88209
* Le destructor.
89210
*/
@@ -742,6 +863,20 @@ void ConnectionManager::processFile(NetFileCommandMsg *msg)
742863
}
743864
#endif // COMPRESS_TARGAS
744865

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+
745880
File *fp = TheFileSystem->openFile(realFileName.str(), File::CREATE | File::BINARY | File::WRITE);
746881
if (fp)
747882
{

0 commit comments

Comments
 (0)