Skip to content
Open
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions Core/GameEngine/Include/Common/FileSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ class FileSystem : public SubsystemInterface

static AsciiString normalizePath(const AsciiString& path); ///< normalizes a file path. The path can refer to a directory. File path must be absolute, but does not need to exist. Returns an empty string on failure.
static Bool isPathInDirectory(const AsciiString& testPath, const AsciiString& basePath); ///< determines if a file path is within a base path. Both paths must be absolute, but do not need to exist.
static Bool hasValidTransferFileContent(const AsciiString& filePath, const UnsignedByte* data, Int dataSize); ///< validates transferred file content in memory before writing to disk.

protected:
#if ENABLE_FILESYSTEM_EXISTENCE_CACHE
Expand Down
118 changes: 118 additions & 0 deletions Core/GameEngine/Source/Common/System/FileSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,121 @@ Bool FileSystem::isPathInDirectory(const AsciiString& testPath, const AsciiStrin

return true;
}

namespace
{

enum TransferFileType
{
TRANSFER_FILE_MAP,
TRANSFER_FILE_INI,
TRANSFER_FILE_STR,
TRANSFER_FILE_TXT,
TRANSFER_FILE_TGA,
TRANSFER_FILE_WAK,
TRANSFER_FILE_COUNT
};

struct TransferFileRule
{
const char* ext;
Int maxSize;
TransferFileType type;
};

const TransferFileRule transferFileRules[] =
{
{ ".map", 5 * 1024 * 1024, TRANSFER_FILE_MAP },
{ ".ini", 512 * 1024, TRANSFER_FILE_INI },
{ ".str", 512 * 1024, TRANSFER_FILE_STR },
{ ".txt", 512 * 1024, TRANSFER_FILE_TXT },
{ ".tga", 2 * 1024 * 1024, TRANSFER_FILE_TGA },
{ ".wak", 512 * 1024, TRANSFER_FILE_WAK },
Comment on lines +484 to +489
Copy link

Choose a reason for hiding this comment

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

Align columns in lookup table
Per project style, data-oriented lookup tables should have aligned columns for readability.

Suggested change
{ ".map", 5 * 1024 * 1024, TRANSFER_FILE_MAP },
{ ".ini", 512 * 1024, TRANSFER_FILE_INI },
{ ".str", 512 * 1024, TRANSFER_FILE_STR },
{ ".txt", 512 * 1024, TRANSFER_FILE_TXT },
{ ".tga", 2 * 1024 * 1024, TRANSFER_FILE_TGA },
{ ".wak", 512 * 1024, TRANSFER_FILE_WAK },
{ ".map", 5 * 1024 * 1024, TRANSFER_FILE_MAP },
{ ".ini", 512 * 1024, TRANSFER_FILE_INI },
{ ".str", 512 * 1024, TRANSFER_FILE_STR },
{ ".txt", 512 * 1024, TRANSFER_FILE_TXT },
{ ".tga", 2 * 1024 * 1024, TRANSFER_FILE_TGA },
{ ".wak", 512 * 1024, TRANSFER_FILE_WAK },

Context Used: Rule from dashboard - Align columns in data-oriented blocks (like lookup tables, initialization lists, or structured data)... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngine/Source/Common/System/FileSystem.cpp
Line: 484:489

Comment:
**Align columns in lookup table**
Per project style, data-oriented lookup tables should have aligned columns for readability.

```suggestion
	{ ".map", 5 * 1024 * 1024, TRANSFER_FILE_MAP },
	{ ".ini", 512 * 1024,      TRANSFER_FILE_INI },
	{ ".str", 512 * 1024,      TRANSFER_FILE_STR },
	{ ".txt", 512 * 1024,      TRANSFER_FILE_TXT },
	{ ".tga", 2 * 1024 * 1024, TRANSFER_FILE_TGA },
	{ ".wak", 512 * 1024,      TRANSFER_FILE_WAK },
```

**Context Used:** Rule from `dashboard` - Align columns in data-oriented blocks (like lookup tables, initialization lists, or structured data)... ([source](https://app.greptile.com/review/custom-context?memory=e61e83cf-a14f-4759-bed8-3bab008b25b5))

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

};

const TransferFileRule* getTransferFileRule(const char* extension)
{
for (Int i = 0; i < ARRAY_SIZE(transferFileRules); ++i)
{
if (stricmp(extension, transferFileRules[i].ext) == 0)
{
return &transferFileRules[i];
}
}
return nullptr;
}

} // namespace

//============================================================================
// FileSystem::hasValidTransferFileContent
//============================================================================
// TheSuperHackers @security bobtista 12/02/2026 Validates transferred file
// content in memory before writing to disk.
Bool FileSystem::hasValidTransferFileContent(const AsciiString& filePath, const UnsignedByte* data, Int dataSize)
{
const char* lastDot = strrchr(filePath.str(), '.');
if (lastDot == nullptr)
{
DEBUG_LOG(("File '%s' has no extension for content validation.", filePath.str()));
return false;
}

const TransferFileRule* rule = getTransferFileRule(lastDot);
if (rule == nullptr)
{
DEBUG_LOG(("File '%s' has unrecognized extension '%s' for content validation.", filePath.str(), lastDot));
return false;
}

// Check size limit
if (dataSize > rule->maxSize)
{
DEBUG_LOG(("File '%s' exceeds maximum size (%d bytes, limit %d bytes).", filePath.str(), dataSize, rule->maxSize));
return false;
}

// Extension-specific content validation
switch (rule->type)
{
case TRANSFER_FILE_MAP:
if (dataSize < 4 || memcmp(data, "CkMp", 4) != 0)
{
DEBUG_LOG(("Map file '%s' has invalid magic bytes.", filePath.str()));
return false;
}
break;

case TRANSFER_FILE_INI:
{
Int bytesToCheck = std::min(dataSize, 512);
for (Int i = 0; i < bytesToCheck; ++i)
{
if (data[i] == 0)
{
DEBUG_LOG(("INI file '%s' contains null bytes (likely binary).", filePath.str()));
return false;
}
}
break;
}

case TRANSFER_FILE_TGA:
if (dataSize < 44)
{
DEBUG_LOG(("TGA file '%s' is too small to be valid (minimum header 18 + footer 26 = 44 bytes).", filePath.str()));
return false;
}
if (memcmp(data + dataSize - 18, "TRUEVISION-XFILE.", 18) != 0)
Copy link

Choose a reason for hiding this comment

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

TGA footer validation is checking wrong offset and size. The TGA 2.0 footer is 26 bytes (see TARGA.h:134-142), not 18. The footer consists of Extension(4) + Developer(4) + Signature[16] + RsvdChar(1) + BZST(1) = 26 bytes total.

Current code checks last 18 bytes, which skips the 8-byte Extension and Developer fields and reads into image data instead of the footer.

Suggested change
if (memcmp(data + dataSize - 18, "TRUEVISION-XFILE.", 18) != 0)
if (memcmp(data + dataSize - 26, "TRUEVISION-XFILE.", 18) != 0)

Also update line 560 to check dataSize < 26 instead of dataSize < 18 since a valid TGA with footer needs at least header(18) + footer(26) = 44 bytes minimum.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngine/Source/Common/System/FileSystem.cpp
Line: 565:565

Comment:
TGA footer validation is checking wrong offset and size. The TGA 2.0 footer is 26 bytes (see `TARGA.h:134-142`), not 18. The footer consists of Extension(4) + Developer(4) + Signature[16] + RsvdChar(1) + BZST(1) = 26 bytes total.

Current code checks last 18 bytes, which skips the 8-byte Extension and Developer fields and reads into image data instead of the footer.

```suggestion
		if (memcmp(data + dataSize - 26, "TRUEVISION-XFILE.", 18) != 0)
```

Also update line 560 to check `dataSize < 26` instead of `dataSize < 18` since a valid TGA with footer needs at least header(18) + footer(26) = 44 bytes minimum.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Author

Choose a reason for hiding this comment

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

The offset is correct — you can verify in Core/Libraries/Source/WWVegas/WWLib/TARGA.h:134-142:

  typedef struct _TGA2Footer {
      long Extension;    // 4 bytes (offset 0 from footer start)
      long Developer;    // 4 bytes (offset 4)
      char Signature[16]; // 16 bytes (offset 8)
      char RsvdChar;     // 1 byte '.' (offset 24)
      char BZST;         // 1 byte '\0' (offset 25)
  } TGA2Footer;          // 26 bytes total

The Signature field starts at footer offset 8, which is EOF - 18 (26 - 8). Our memcmp(data +
dataSize - 18, "TRUEVISION-XFILE.", 18) checks exactly Signature + RsvdChar + BZST — the last 18
bytes. Starting at dataSize - 26 would read the Extension and Developer fields instead.

{
DEBUG_LOG(("TGA file '%s' is missing TRUEVISION-XFILE footer signature.", filePath.str()));
return false;
}
break;

default:
break;
}

return true;
}
15 changes: 15 additions & 0 deletions Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -734,6 +735,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 (!FileSystem::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)
{
Expand Down
Loading