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
@@ -3,6 +3,7 @@ using System.Text;
using System.Text.RegularExpressions;
using Strata.SqlTools.SqlBreakdown.Classes;
using Strata.SqlTools.SqlBreakdown.Enums.SQL;
using Strata.SqlTools.SqlBreakdown.Utilities;
namespace Strata.SqlTools.Statements.SqlServer;
@@ -35,7 +36,7 @@ public class StatementParser
// Remove SQL comments before processing
sql = RemoveSqlComments(sql);
// Replace multiple whitespace/newlines with single space
sql = Regex.Replace(sql, @"\s+", " ");
sql = Regex.Replace(sql, @"\s+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
return sql.Trim();
}
@@ -47,7 +48,7 @@ public class StatementParser
public virtual string NormalizeSqlPreservingComments(string sql)
{
// Replace multiple spaces/tabs with single space, but preserve newlines for comment handling
sql = Regex.Replace(sql, @"[ \t]+", " ");
sql = Regex.Replace(sql, @"[ \t]+", " ", RegexOptions.None, RegexDefaults.MatchTimeout);
// Remove leading/trailing whitespace from each line
var lines = sql.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
sql = string.Join("\n", lines.Select(line => line.Trim()));
@@ -238,7 +239,7 @@ public class StatementParser
// Simple extraction: look for statements before the main SELECT
var selectIndex = Regex.Match(
sql, @"\bSELECT\b", RegexOptions.IgnoreCase).Index;
sql, @"\bSELECT\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout).Index;
if (selectIndex > 0)
{
@@ -275,7 +276,7 @@ public class StatementParser
// Look for DROP TABLE or other cleanup statements after the main query
var finishPattern = GetFinishClausePattern();
var match = Regex.Match(
sql, finishPattern, RegexOptions.IgnoreCase);
sql, finishPattern, RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
if (match.Success)
{
@@ -302,7 +303,7 @@ public class StatementParser
// Check if SQL starts with WITH
var withMatch = Regex.Match(
sql, @"^\s*WITH\b", RegexOptions.IgnoreCase);
sql, @"^\s*WITH\b", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
if (!withMatch.Success)
{
@@ -330,7 +331,7 @@ public class StatementParser
// Check if we're at a SELECT keyword at top level
var selectMatch = Regex.Match(
sql.Substring(i), @"^\s*SELECT\b",
RegexOptions.IgnoreCase);
RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
if (selectMatch.Success && IsTopLevelKeyword(sql, i + selectMatch.Index))
{
@@ -398,7 +399,7 @@ public class StatementParser
}
if (!Regex.IsMatch(sqlTrimmed, @"^\s*(WITH|SELECT)\b",
RegexOptions.IgnoreCase))
RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout))
{
errorMessage = "SQL statement must start with WITH or SELECT.";
clauses = null;
@@ -830,7 +831,7 @@ public class StatementParser
/// <param name="paramPattern">The regex pattern to match parameter names.</param>
protected virtual void ExtractParameters(Dictionary<string, object> parameters, string sql, string paramPattern)
{
var matches = Regex.Matches(sql, paramPattern);
var matches = Regex.Matches(sql, paramPattern, RegexOptions.None, RegexDefaults.MatchTimeout);
var paramNames = matches.Cast<Match>()
.Select(match => match.Value)