Review regular expressions without a match timeout (ReDoS)
Matches methods that create or run a Regex through an overload without a TimeSpan matchTimeout argument (constructors, Regex.IsMatch(), Match(), Matches(), Replace(), Split(), Count(), EnumerateMatches()) or that declare a [GeneratedRegex] without the matchTimeoutMilliseconds argument. The .NET backtracking engine can be driven into catastrophic backtracking by crafted input, a Regular Expression Denial of Service (ReDoS): https://learn.microsoft.com/dotnet/standard/base-types/best-practices-regex#use-time-out-values The rule is muted when the application sets the process-wide REGEX_DEFAULT_MATCH_TIMEOUT through AppDomain.SetData().
This is CWE-1333 Inefficient Regular Expression Complexity.
Flagged Code
This is CWE-1333 Inefficient Regular Expression Complexity.
Flagged Code
public bool IsValidTitle(string title)
{
return Regex.IsMatch(title, @"^(\w+\s?)*$"); // flagged: no matchTimeout, nested quantifier
}
Fixed Codestatic readonly Regex s_Title =
new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(200));
public bool IsValidTitle(string title)
{
return s_Title.IsMatch(title); // instance call, timeout set at construction
}How to fix
Pass a matchTimeout (a few hundred milliseconds is usually plenty), or opt into the linear-time engine with RegexOptions.NonBacktracking (.NET 7+), or set a process-wide default with AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromMilliseconds(500)). Suppress the issue when the pattern only ever runs on trusted, bounded input.
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Review regular expressions without a match timeout (ReDoS)</Name>
// <Id>ND3112:ReviewRegexWithoutMatchTimeout</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
let regexType = ThirdParty.Types.WithFullName("System.Text.RegularExpressions.Regex")
where regexType.Any()
// A process-wide default timeout makes every regex safe.
let appSetsDefaultTimeout = Application.Methods.UsingAny(
ThirdParty.Methods.WithFullName("System.AppDomain.SetData(String,Object)")).Any()
where !appSetsDefaultTimeout
// Constructors and static helpers that take a pattern but no TimeSpan matchTimeout.
let regexMembersWithoutTimeout = regexType.ChildMethods()
.Where(m => (m.IsConstructor || m.IsStatic) &&
m.IsPublic &&
m.NbParameters > 0 &&
!m.Name.Contains("TimeSpan") &&
m.SimpleNameEqualsAny(".ctor", "IsMatch", "Match", "Matches", "Replace", "Split", "Count", "EnumerateMatches", "EnumerateSplits"))
.ToHashSetEx()
// [GeneratedRegex(pattern, options, matchTimeoutMilliseconds)]: the third argument is the timeout when it is an integer.
let generatedRegexAttr = ThirdParty.Types.WithFullName("System.Text.RegularExpressions.GeneratedRegexAttribute").FirstOrDefault()
let hasNoTimeoutArg = new Func<IAttributeTag, bool>(tag =>
!tag.Parameters.Any(p => p.Name == "Arg2" && p.Value.ToString().All(c => Char.IsDigit(c))))
let generatedRegexMethods = generatedRegexAttr == null ? new IMethod[0] :
Application.Methods.TaggedWithAttribute(generatedRegexAttr).ToArray()
from m in Application.Methods.UsingAny(regexMembersWithoutTimeout).Union(generatedRegexMethods)
let regexMembersUsed = m.MethodsCalled.Where(x => regexMembersWithoutTimeout.Contains(x)).ToArray()
let isGeneratedRegexWithoutTimeout = generatedRegexAttr != null && m.AttributeTagsOf(generatedRegexAttr).Any(tag => hasNoTimeoutArg(tag))
where regexMembersUsed.Length > 0 || isGeneratedRegexWithoutTimeout
select new {
m,
regexMembersUsed,
isGeneratedRegexWithoutTimeout,
Debt = (5 + 3 * regexMembersUsed.Length).ToMinutes().ToDebt(),
Severity = regexMembersUsed.Length > 0 ? Severity.Medium : Severity.Low
}
//<Expl>
// The *{0}* {2?is a [GeneratedRegex] declared without a match timeout:uses regular expressions without a match timeout: {1}}.
// A pattern with nested quantifiers applied to hostile input can take
// exponential time and pin a CPU core.
//</Expl>
//<Desc>
// Matches methods that create or run a *Regex* through an overload without
// a *TimeSpan matchTimeout* argument (constructors, *Regex.IsMatch()*,
// *Match()*, *Matches()*, *Replace()*, *Split()*, *Count()*,
// *EnumerateMatches()*) or that declare a *[GeneratedRegex]* without the
// *matchTimeoutMilliseconds* argument. The .NET backtracking engine can be
// driven into catastrophic backtracking by crafted input, a Regular
// Expression Denial of Service (ReDoS):
// https://learn.microsoft.com/dotnet/standard/base-types/best-practices-regex#use-time-out-values
// The rule is muted when the application sets the process-wide
// *REGEX_DEFAULT_MATCH_TIMEOUT* through *AppDomain.SetData()*.
//
// This is CWE-1333 *Inefficient Regular Expression Complexity*.
//
// **Flagged Code**
// ```csharp
// public bool IsValidTitle(string title)
// {
// return Regex.IsMatch(title, @"^(\w+\s?)*$"); // flagged: no matchTimeout, nested quantifier
// }
// ```
//
// **Fixed Code**
// ```csharp
// static readonly Regex s_Title =
// new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(200));
//
// public bool IsValidTitle(string title)
// {
// return s_Title.IsMatch(title); // instance call, timeout set at construction
// }
// ```
//</Desc>
//<HowToFix>
// Pass a *matchTimeout* (a few hundred milliseconds is usually plenty), or
// opt into the linear-time engine with *RegexOptions.NonBacktracking*
// (.NET 7+), or set a process-wide default with
// *AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromMilliseconds(500))*.
// Suppress the issue when the pattern only ever runs on trusted,
// bounded input.
//</HowToFix>
Open in the interactive Rules Explorer · All default rules
Security: all 39 items
- Software Composition Analysis (SCA) ND3120
- Transitive Software Composition Analysis (SCA) ND3121
- Don't hard-code secrets in constants ND3114
- Review credentials built from hard-coded values ND3115
- Don't use insecure deserializers ND3110
- Don't use weak hash algorithms (MD5, SHA1) ND3109
- Don't use DES/3DES/RC2 weak cipher algorithms ND3102
- Don't use weak password-based key derivation ND3123
- Don't use the ECB encryption mode ND3132
- Don't disable certificate validation ND3103
- Don't hard-code TLS/SSL protocol versions ND3122
- Review commands vulnerable to SQL injection ND3107
- Review data adapters vulnerable to SQL injection ND3108
- Review raw SQL built from composed strings ND3128
- Review LDAP filters composed from strings ND3137
- Don't enable external XML entity resolution (XXE) ND3111
- Review processes started with a composed arguments string ND3124
- Review web endpoints building file paths from their parameters ND3129
- Review custom archive extraction (Zip Slip) ND3135
- Review assemblies and types loaded from runtime-provided names ND3125
- Review web endpoints without [Authorize] nor [AllowAnonymous] ND3118
- Review state-changing endpoints opened to anonymous callers ND3130
- Review state-changing MVC actions without anti-forgery validation ND3119
- Review HTML-encoding bypasses ND3133
- Review explicitly disabled request validation ND3134
- Review redirects to a URL that is not validated as local ND3126
- Review request-influenced outbound HTTP calls (SSRF) ND3136
- Review insecure ASP.NET Core CORS policies and unguarded developer exception page ND3116
- Review cookies created without HttpOnly and Secure flags ND3117
- Review regular expressions without a match timeout (ReDoS) ND3112
- Review secrets compared with non-constant-time equality ND3113
- Review secrets passed to logging methods ND3127
- Review security operations that log nothing ND3131
- Don't use System.Random for security purposes ND3101
- Don't use CoSetProxyBlanket and CoInitializeSecurity ND3100
- Review publicly visible event handlers ND3104
- Pointers should not be publicly visible ND3105
- Seal methods that satisfy non-public interfaces ND3106
- Restrict sensitive APIs to reviewed components ND3138