Table of Contents

Class ChatSanitizer

Namespace
FishMMO.Shared
Assembly
FishMMO-DiscordBot.dll

Text-hygiene routines applied to every piece of untrusted text that enters the chat pipeline — player input, and anything bridged in from Discord.

public static class ChatSanitizer
Inheritance
ChatSanitizer
Inherited Members

Remarks

Deliberately free of every project dependency (no logging, no Unity, no character types) so the whole file can be compiled and exercised on its own. This is security code that is cheap to test and expensive to get wrong, and the old implementation lived inside ChatHelper where nothing could reach it without dragging the server's whole object graph along.

Everything here is fail closed: a call that cannot complete its work returns the empty string rather than a partially-cleaned one. The previous behaviour on a regex timeout was the opposite — it returned the input truncated to 256 characters with every rich-text tag still in it, which handed an attacker a guaranteed bypass: make the pattern slow, and the sanitiser stops sanitising.

Fields

CombinedRichTextPattern

Combined regex pattern for all supported Unity Rich Text tags.

public static readonly string CombinedRichTextPattern

Field Value

string

MaxPasses

Maximum number of removal passes made before the input is rejected outright.

public const int MaxPasses = 8

Field Value

int

Remarks

A single pass is not enough. Removing a match can splice its neighbours together into a brand new tag: <siz<size=500>e=500> contains exactly one match (<size=500>), and deleting it leaves <size=500> behind — the tag the filter was written to remove. Nesting that trick n deep needs n passes, so the loop runs to a fixed point.

The cap exists because "run until nothing changes" on attacker-chosen input is an unbounded loop on the game loop. Reaching it means the input is still growing new tags after this many rounds, which no honest message does, so it is rejected.

Methods

SanitizeIncoming(string, int)

The complete inbound pipeline for untrusted chat text.

public static string SanitizeIncoming(string message, int maxLength)

Parameters

message string

Untrusted text, from a player or from the Discord bridge.

maxLength int

Hard character cap applied after cleaning. Values below one disable truncation.

Returns

string

Clean, single-line, tag-free, code-free text within maxLength, or Empty if nothing survived.

Remarks

Order matters. Control characters go first, because they can be used to break a tag or a control code apart so the later passes do not recognise it. Rich text goes next, and the control-code prefix last, because removing a tag from the middle of FISH<b>MMO_ is what would otherwise reassemble the prefix.

Truncation happens at the very end and is a hard cut, not a "…" — the ellipsis the old timeout path appended was three extra characters appearing on a length-limited field.

StripChatCodes(string)

Removes the FISHMMO_ control-code prefix wherever it appears, repeating until the text stops changing.

public static string StripChatCodes(string message)

Parameters

message string

Untrusted text.

Returns

string

The text with no control-code prefix left in it, or empty on failure.

Remarks

The chat protocol carries a handful of in-band control codes — FISHMMO_TELL_RELAYED, FISHMMO_TARGET_OFFLINE and friends — which the client matches on the first word of a message and renders specially. They were never stripped from player input, so /tell Bob FISHMMO_TELL_RELAYED you owe me gold arrived at Bob's client looking exactly like a whisper Bob had sent to someone else. Removing the prefix is enough to defuse every code at once, and the codes the server itself emits are prepended after this runs.

Looped for the same reassembly reason as StripRichText(string): FISHMMOFISHMMO__ collapses to FISHMMO_ in one pass.

StripControlCharacters(string)

Folds control characters and invisible formatting characters out of the text.

public static string StripControlCharacters(string message)

Parameters

message string

Untrusted text.

Returns

string

Single-line text containing no control or format characters.

Remarks

Two separate problems, both of which reached the chat log unfiltered:

  • Newlines. Every chat row is a single-line record; a message carrying \n renders as several lines in a wrapping Label, which lets one player push everyone else's chat off the top of the window from inside a 128-character budget. Line breaks and tabs become a space so words either side of them stay separated.
  • Unicode Format characters — U+202E RIGHT-TO-LEFT OVERRIDE above all. It reverses the display order of everything after it, so a message can be made to render as a different message entirely (including impersonating another player's name), and it leaks out of the message into the rest of the line. Zero-width spaces and joiners are in the same category and are what defeats a "no duplicate messages" filter.

Surrogate pairs are left alone — emoji are fine.

StripRichText(string)

Removes every Unity Rich Text tag from message, repeating until the text stops changing.

public static string StripRichText(string message)

Parameters

message string

Untrusted text.

Returns

string

The text with all recognised tags removed, or Empty when the input could not be cleaned inside MaxPasses passes or FishMMO.Shared.ChatSanitizer.TotalBudgetMilliseconds.