fix(security): Resolve SonarQube security hotspots
SonarQube Analysis / sonarqube (pull_request) Successful in 3m9s

Introduce a default regex match timeout across the library to prevent potential ReDoS attacks (SonarQube rule S6444).
Implement `[OnDeserialized]` methods to re-establish object invariants and validate state after deserialization, addressing SonarQube rule S5766.
This commit is contained in:
Thom Lamb
2026-05-20 17:19:17 -05:00
parent df1805a402
commit e3153e58c4
26 changed files with 268 additions and 58 deletions
@@ -8,6 +8,12 @@ namespace Strata.SqlTools.Rules.Rule.Expression;
/// </summary>
public static class Markdown
{
/// <summary>
/// Match timeout applied to all regular expressions to guard against catastrophic
/// backtracking / ReDoS denial-of-service attacks (SonarQube rule S6444).
/// </summary>
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
private static readonly Dictionary<string, Func<Expression, Expression, BoolExpr>> LogicalOperators = new()
{
{ "\\land", (left, right) => new And((BoolExpr)left, (BoolExpr)right) },
@@ -70,12 +76,12 @@ public static class Markdown
private static string StripMarkdownDelimiters(string text)
{
// Remove $...$ or $$...$$ delimiters
text = Regex.Replace(text, @"^\$\$?\s*", "");
text = Regex.Replace(text, @"\s*\$\$?$", "");
text = Regex.Replace(text, @"^\$\$?\s*", "", RegexOptions.None, RegexTimeout);
text = Regex.Replace(text, @"\s*\$\$?$", "", RegexOptions.None, RegexTimeout);
// Remove ```math...``` code fence
text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline);
text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline);
text = Regex.Replace(text, @"^```math\s*", "", RegexOptions.Multiline, RegexTimeout);
text = Regex.Replace(text, @"\s*```$", "", RegexOptions.Multiline, RegexTimeout);
return text.Trim();
}
@@ -186,18 +192,18 @@ public static class Markdown
private static Expression? TryParseProperty(string text)
{
// Parse property access (e.g., x.PropertyName or \text{x.PropertyName})
var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
var propertyMatch = Regex.Match(text, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$", RegexOptions.None, RegexTimeout);
if (propertyMatch.Success)
{
return new Property(propertyMatch.Groups[1].Value, propertyMatch.Groups[2].Value);
}
// Parse \text{...} property access
var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$");
var textMatch = Regex.Match(text, @"^\\text\{([^}]+)\}$", RegexOptions.None, RegexTimeout);
if (textMatch.Success)
{
var textContent = textMatch.Groups[1].Value;
var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$");
var propMatch = Regex.Match(textContent, @"^([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)$", RegexOptions.None, RegexTimeout);
if (propMatch.Success)
{
return new Property(propMatch.Groups[1].Value, propMatch.Groups[2].Value);
@@ -215,7 +221,7 @@ public static class Markdown
}
// Single property name
if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
if (Regex.IsMatch(textContent, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.None, RegexTimeout))
{
return new Property(textContent);
}
@@ -236,7 +242,7 @@ public static class Markdown
}
// Parse simple property without parameter
if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
if (Regex.IsMatch(text, @"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.None, RegexTimeout))
{
return new Property(text);
}
@@ -247,7 +253,7 @@ public static class Markdown
private static Expression? TryParseLiteral(string text)
{
// Parse string literals (quoted)
var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$");
var stringMatch = Regex.Match(text, @"^[""'](.+?)[""']$", RegexOptions.None, RegexTimeout);
if (stringMatch.Success)
{
return new StringLiteral(stringMatch.Groups[1].Value);