diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs
index 079d4f9ab8..4dc5c60a07 100644
--- a/Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs
+++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs
@@ -14,6 +14,13 @@ public class AnsiKeyboardParser
new EscAsAltPattern { IsLastMinute = true }
];
+ ///
+ /// Maximum input length for keyboard escape sequences. Real keyboard sequences are short
+ /// (typically under 20 characters). This guard prevents pattern evaluation against
+ /// pathologically large inputs accumulated by the parser.
+ ///
+ internal const int MaxKeyboardSequenceLength = 64;
+
///
/// Looks for any pattern that matches the and returns
/// the matching pattern or if no matches.
@@ -23,6 +30,11 @@ public class AnsiKeyboardParser
///
public AnsiKeyboardParserPattern? IsKeyboard (string? input, bool isLastMinute = false)
{
+ if (input is null || input.Length > MaxKeyboardSequenceLength)
+ {
+ return null;
+ }
+
return _patterns.FirstOrDefault (pattern => pattern.IsLastMinute == isLastMinute && pattern.IsMatch (input));
}
}
diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs
index fcd4d8aae9..21bc3971ec 100644
--- a/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs
+++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiMouseParser.cs
@@ -62,6 +62,13 @@ public class AnsiMouseParser
// Regex patterns for button press/release, wheel scroll, and mouse position reporting
private readonly Regex _mouseEventPattern = new (@"\u001b\[<(\d+);(\d+);(\d+)(M|m)", RegexOptions.Compiled);
+ ///
+ /// Maximum input length for mouse escape sequences. Real mouse sequences are short
+ /// (typically under 20 characters). This guard prevents regex evaluation against
+ /// pathologically large inputs accumulated by the parser.
+ ///
+ internal const int MaxMouseSequenceLength = 64;
+
///
/// Returns true if it is a mouse event
///
@@ -71,7 +78,7 @@ public bool IsMouse (string? cur) =>
// Typically in this format
// ESC [ < {button_code};{x_pos};{y_pos}{final_byte}
- cur!.EndsWith ('M') || cur.EndsWith ('m');
+ cur is { Length: <= MaxMouseSequenceLength } && (cur.EndsWith ('M') || cur.EndsWith ('m'));
///
/// Parses a mouse ansi escape sequence into a mouse event. Returns null if input
@@ -81,8 +88,13 @@ public bool IsMouse (string? cur) =>
///
public Mouse? ProcessMouseInput (string? input)
{
+ if (input is null || input.Length > MaxMouseSequenceLength)
+ {
+ return null;
+ }
+
// Match mouse wheel events first
- Match match = _mouseEventPattern.Match (input!);
+ Match match = _mouseEventPattern.Match (input);
if (!match.Success)
{
diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs
index aa8d903fa5..ddf658a7f9 100644
--- a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs
+++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseParserBase.cs
@@ -12,6 +12,13 @@ internal abstract class AnsiResponseParserBase (IHeld heldContent, ITimeProvider
private const char ESCAPE = '\x1B';
private const char BEL = '\a';
+ ///
+ /// Maximum number of characters that can be accumulated in held content before the parser
+ /// abandons the current escape sequence and releases buffered content. This prevents unbounded
+ /// memory growth from malformed or malicious unterminated escape sequences.
+ ///
+ internal const int MaxHeldLength = 8 * 1024;
+
///
/// Tracks whether the parser is currently inside an OSC (Operating System Command) sequence.
/// OSC responses can be terminated by ST (ESC \) which requires special handling because
@@ -164,6 +171,15 @@ private void ProcessInputBaseImpl (Func getCharAtIndex, Func= MaxHeldLength)
+ {
+ ReleaseHeld (appendOutput);
+ appendOutput (currentObj);
+
+ break;
+ }
+
if (_inOscSequence)
{
if (_oscExpectingBackslash)
diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiParserSecurityTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiParserSecurityTests.cs
new file mode 100644
index 0000000000..883c37c2cb
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiParserSecurityTests.cs
@@ -0,0 +1,174 @@
+// Copilot - Claude Sonnet 4
+
+using System.Text;
+
+namespace DriverTests.AnsiHandling;
+
+///
+/// Tests that verify the ANSI parser guards against unbounded memory growth
+/// from malformed or malicious unterminated escape sequences.
+///
+[Collection ("Driver Tests")]
+public class AnsiParserSecurityTests
+{
+ [Fact]
+ public void Parser_ReleasesHeldContent_WhenMaxLengthExceeded_CSI ()
+ {
+ AnsiResponseParser parser = new (new SystemTimeProvider ());
+
+ // Build an unterminated CSI sequence longer than the max held length.
+ // CSI starts with ESC [ then we fill with parameter bytes (digits/semicolons) without a terminator.
+ StringBuilder input = new ();
+ input.Append ("\x1b["); // CSI introducer
+
+ // Fill with parameter bytes beyond the max held length
+ int fillLength = AnsiResponseParserBase.MaxHeldLength + 100;
+
+ for (var i = 0; i < fillLength; i++)
+ {
+ input.Append ('0');
+ }
+
+ // Process the input — should not throw and should not accumulate unbounded memory
+ string released = parser.ProcessInput (input.ToString ());
+
+ // The parser should have released the held content once it exceeded the limit
+ // The released output should contain the original characters (released back as output)
+ Assert.True (released.Length > 0);
+
+ // Parser should be back in Normal state after release
+ Assert.Equal (AnsiResponseParserState.Normal, parser.State);
+ }
+
+ [Fact]
+ public void Parser_ReleasesHeldContent_WhenMaxLengthExceeded_OSC ()
+ {
+ AnsiResponseParser parser = new (new SystemTimeProvider ());
+
+ // Build an unterminated OSC sequence longer than the max held length.
+ // OSC starts with ESC ] then we fill with arbitrary content without a terminator (BEL or ST).
+ StringBuilder input = new ();
+ input.Append ("\x1b]"); // OSC introducer
+
+ int fillLength = AnsiResponseParserBase.MaxHeldLength + 100;
+
+ for (var i = 0; i < fillLength; i++)
+ {
+ input.Append ('x');
+ }
+
+ string released = parser.ProcessInput (input.ToString ());
+
+ // The parser should have released the held content once it exceeded the limit
+ Assert.True (released.Length > 0);
+ Assert.Equal (AnsiResponseParserState.Normal, parser.State);
+ }
+
+ [Fact]
+ public void Parser_NormalSequences_StillWork_AfterOverflow ()
+ {
+ AnsiResponseParser parser = new (new SystemTimeProvider ()) { HandleMouse = true };
+
+ // First, overflow the parser with an unterminated sequence
+ StringBuilder overflow = new ();
+ overflow.Append ("\x1b[");
+
+ for (var i = 0; i < AnsiResponseParserBase.MaxHeldLength + 10; i++)
+ {
+ overflow.Append ('0');
+ }
+
+ parser.ProcessInput (overflow.ToString ());
+
+ // Now send a valid mouse sequence — it should still be detected
+ List mouseEvents = [];
+ parser.Mouse += (_, e) => mouseEvents.Add (e);
+
+ parser.ProcessInput ("\x1b[<0;10;20M");
+
+ Assert.Single (mouseEvents);
+ }
+
+ [Fact]
+ public void MouseParser_RejectsOversizedInput_IsMouse ()
+ {
+ AnsiMouseParser parser = new ();
+
+ // Normal mouse sequence
+ Assert.True (parser.IsMouse ("\x1b[<0;10;20M"));
+
+ // Oversized input should be rejected
+ string oversized = "\x1b[<" + new string ('0', AnsiMouseParser.MaxMouseSequenceLength + 10) + "M";
+ Assert.False (parser.IsMouse (oversized));
+ }
+
+ [Fact]
+ public void MouseParser_RejectsOversizedInput_ProcessMouseInput ()
+ {
+ AnsiMouseParser parser = new ();
+
+ // Normal mouse sequence should work
+ Mouse? result = parser.ProcessMouseInput ("\x1b[<0;10;20M");
+ Assert.NotNull (result);
+
+ // Oversized input should return null
+ string oversized = "\x1b[<" + new string ('0', AnsiMouseParser.MaxMouseSequenceLength + 10) + ";10;20M";
+ result = parser.ProcessMouseInput (oversized);
+ Assert.Null (result);
+ }
+
+ [Fact]
+ public void MouseParser_RejectsNull_ProcessMouseInput ()
+ {
+ AnsiMouseParser parser = new ();
+ Mouse? result = parser.ProcessMouseInput (null);
+ Assert.Null (result);
+ }
+
+ [Fact]
+ public void KeyboardParser_RejectsOversizedInput ()
+ {
+ AnsiKeyboardParser parser = new ();
+
+ // Normal keyboard sequence should work
+ AnsiKeyboardParserPattern? result = parser.IsKeyboard ("\x1b[A");
+ Assert.NotNull (result);
+
+ // Oversized input should return null
+ string oversized = "\x1b[" + new string ('1', AnsiKeyboardParser.MaxKeyboardSequenceLength + 10) + "A";
+ result = parser.IsKeyboard (oversized);
+ Assert.Null (result);
+ }
+
+ [Fact]
+ public void KeyboardParser_RejectsNull ()
+ {
+ AnsiKeyboardParser parser = new ();
+ AnsiKeyboardParserPattern? result = parser.IsKeyboard (null);
+ Assert.Null (result);
+ }
+
+ [Fact]
+ public void Parser_GenericVariant_ReleasesHeldContent_WhenMaxLengthExceeded ()
+ {
+ AnsiResponseParser parser = new (new SystemTimeProvider ());
+
+ // Build unterminated CSI sequence
+ List> input = [];
+ input.Add (Tuple.Create ('\x1b', 0));
+ input.Add (Tuple.Create ('[', 1));
+
+ int fillLength = AnsiResponseParserBase.MaxHeldLength + 100;
+
+ for (var i = 0; i < fillLength; i++)
+ {
+ input.Add (Tuple.Create ('0', i + 2));
+ }
+
+ IEnumerable> released = parser.ProcessInput (input.ToArray ());
+
+ // Should release content and not accumulate unbounded memory
+ Assert.True (released.Any ());
+ Assert.Equal (AnsiResponseParserState.Normal, parser.State);
+ }
+}