mirror of
https://github.com/2dust/v2rayN.git
synced 2026-09-22 22:38:10 +03:00
fix(security): guard subscription/message regex filters against ReDoS (#10185)
Filter patterns (subscription subFilter, policy-group Filter, message MsgFilter) accept arbitrary user input while the tested text (remarks from subscriptions, log lines) is attacker-influenced. Regex.IsMatch without timeout hangs on evil patterns like (a+)+$ - a malicious subscription can freeze the UI/log pipeline on every update. Add Utils.IsRegexMatch with a 2s timeout; fail open (match) with a log so no node or message is silently dropped. Apply to all four call sites. Co-authored-by: sdhfsl <sdhfsl@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
namespace ServiceLib.Tests.Helper;
|
||||
|
||||
public class RegexGuardTests
|
||||
{
|
||||
[Test]
|
||||
public async Task IsRegexMatch_NormalPattern_ShouldMatch()
|
||||
{
|
||||
await Utils.IsRegexMatch("HK-node-01", "HK|香港").Should().BeTrue();
|
||||
await Utils.IsRegexMatch("JP-node-01", "HK|香港").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsRegexMatch_EmptyPattern_ShouldPassThrough()
|
||||
{
|
||||
await Utils.IsRegexMatch("anything", "").Should().BeTrue();
|
||||
await Utils.IsRegexMatch("anything", null).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsRegexMatch_InvalidPattern_ShouldFailOpen()
|
||||
{
|
||||
await Utils.IsRegexMatch("node-01", "([unclosed").Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsRegexMatch_EvilPattern_ShouldTimeoutAndFailOpen()
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var result = Utils.IsRegexMatch(new string('a', 30) + "!", "(a+)+$");
|
||||
sw.Stop();
|
||||
|
||||
await result.Should().BeTrue();
|
||||
await (sw.Elapsed < TimeSpan.FromSeconds(30)).Should().BeTrue().Because(
|
||||
$"evil pattern must be cut off by timeout, took {sw.Elapsed}");
|
||||
}
|
||||
}
|
||||
@@ -726,6 +726,40 @@ public class Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regex match with a timeout guard. Filter patterns can come from user
|
||||
/// input or subscription content while the tested text (remarks, log
|
||||
/// messages) is attacker-influenced, so an evil pattern like (a+)+$
|
||||
/// would otherwise hang the caller (ReDoS). On timeout or invalid
|
||||
/// pattern, fail open (return true) so no node/message is silently
|
||||
/// dropped; the incident is logged.
|
||||
/// </summary>
|
||||
public static bool IsRegexMatch(string? input, string? pattern, int timeoutSeconds = 2)
|
||||
{
|
||||
if (pattern.IsNullOrEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (input.IsNullOrEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
return Regex.IsMatch(input, pattern, RegexOptions.None, TimeSpan.FromSeconds(timeoutSeconds));
|
||||
}
|
||||
catch (RegexMatchTimeoutException ex)
|
||||
{
|
||||
Logging.SaveLog("IsRegexMatch timeout", ex);
|
||||
return true;
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Logging.SaveLog("IsRegexMatch invalid pattern", ex);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Data Checks
|
||||
|
||||
#region Speed Test
|
||||
|
||||
@@ -1528,7 +1528,7 @@ public static class ConfigHandler
|
||||
p != null &&
|
||||
p.IsValid() &&
|
||||
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
|
||||
Utils.IsRegexMatch(p.Remarks, extraItem.Filter)
|
||||
)
|
||||
.ToList() ?? [];
|
||||
if (matchedChildProfiles.Count == 0)
|
||||
@@ -1667,7 +1667,7 @@ public static class ConfigHandler
|
||||
//exist sub items //filter
|
||||
if (isSub && subid.IsNotEmpty() && subFilter.IsNotEmpty())
|
||||
{
|
||||
if (!Regex.IsMatch(profileItem.Remarks, subFilter))
|
||||
if (!Utils.IsRegexMatch(profileItem.Remarks, subFilter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public class GroupProfileManager
|
||||
p != null &&
|
||||
p.IsValid() &&
|
||||
(!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
|
||||
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
|
||||
Utils.IsRegexMatch(p.Remarks, extra.Filter)
|
||||
)
|
||||
.ToList() ?? [];
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public partial class MsgViewModel : MyReactiveObject
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Regex.IsMatch(msg, MsgFilter))
|
||||
if (!Utils.IsRegexMatch(msg, MsgFilter))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user