diff --git a/src/Strata.SqlTools.LinqToSql/Comparers/QueryComparator.cs b/src/Strata.SqlTools.LinqToSql/Comparers/QueryComparator.cs index 8bc6334..70fe0fc 100644 --- a/src/Strata.SqlTools.LinqToSql/Comparers/QueryComparator.cs +++ b/src/Strata.SqlTools.LinqToSql/Comparers/QueryComparator.cs @@ -221,7 +221,7 @@ public class QueryComparator // Normalize whitespace and case return System.Text.RegularExpressions.Regex - .Replace(clause.Trim(), @"\s+", " ") + .Replace(clause.Trim(), @"\s+", " ", System.Text.RegularExpressions.RegexOptions.None, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout) .ToUpperInvariant(); } diff --git a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdown.cs b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdown.cs index 4429de3..54f187c 100644 --- a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdown.cs +++ b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdown.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.Serialization; using Strata.SqlTools.SqlBreakdown.Expressions; using Strata.SqlTools.SqlBreakdown.Interfaces.Core; using CommandVisitor = Strata.SqlTools.Visitors.PostgreSql.CommandVisitor; @@ -88,6 +89,14 @@ public class QueryBreakdown : SqlServerQueryBreakdown } } + /// + /// Re-establishes invariants after deserialization, since deserialization bypasses the + /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState(); + /// /// Adds a parameter to the query using PostgreSQL's positional parameter format ($1, $2, ...). /// diff --git a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs index 36d71ef..5a1b308 100644 --- a/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs +++ b/src/Strata.SqlTools.PostgreSql/Breakdowns/QueryBreakdownCollection.cs @@ -1,3 +1,4 @@ +using System.Runtime.Serialization; using System.Text; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Interfaces; @@ -35,6 +36,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection _queryBreakdowns = queryBreakdowns?.ToList() ?? new List(); } + /// + /// Validates that the backing list survived deserialization, since deserialization bypasses + /// the constructors that normally initialize it (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (_queryBreakdowns is null) + { + throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list."); + } + } + /// /// Gets the collection of QueryBreakdown objects. /// diff --git a/src/Strata.SqlTools.Query/CalculationFilterGroup.cs b/src/Strata.SqlTools.Query/CalculationFilterGroup.cs index 29653ee..84e0fce 100644 --- a/src/Strata.SqlTools.Query/CalculationFilterGroup.cs +++ b/src/Strata.SqlTools.Query/CalculationFilterGroup.cs @@ -2,18 +2,24 @@ using System.Text.Json.Serialization; namespace Strata.SqlTools.Query; -[method: JsonConstructor] -public class CalculationFilterGroup(IEnumerable filters, LogicalOperator logicalOperator) +public class CalculationFilterGroup { - // Hereditary logical operation applied to all Filters - public LogicalOperator LogicalOperator { get; set; } = logicalOperator; - - public IEnumerable Filters { get; set; } = filters; + [JsonConstructor] + public CalculationFilterGroup(IEnumerable filters, LogicalOperator logicalOperator) + { + Filters = filters; + LogicalOperator = logicalOperator; + } public CalculationFilterGroup() : this([], LogicalOperator.And) { } + // Hereditary logical operation applied to all Filters + public LogicalOperator LogicalOperator { get; set; } + + public IEnumerable Filters { get; set; } + public IEnumerable GetValidFilters() { return Filters?.Where(x => x.IsValid()).ToList() ?? []; diff --git a/src/Strata.SqlTools.Rules/Rule/Expression/Markdown.cs b/src/Strata.SqlTools.Rules/Rule/Expression/Markdown.cs index d5e7c32..ca1c590 100644 --- a/src/Strata.SqlTools.Rules/Rule/Expression/Markdown.cs +++ b/src/Strata.SqlTools.Rules/Rule/Expression/Markdown.cs @@ -8,6 +8,12 @@ namespace Strata.SqlTools.Rules.Rule.Expression; /// public static class Markdown { + /// + /// Match timeout applied to all regular expressions to guard against catastrophic + /// backtracking / ReDoS denial-of-service attacks (SonarQube rule S6444). + /// + private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1); + private static readonly Dictionary> 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); diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs index d0708db..3bfb2b6 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/DeleteBreakdown.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.Serialization; using System.Text; using SqlServerDeleteBreakdown = Strata.SqlTools.Breakdowns.SqlServer.DeleteBreakdown; using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser; @@ -40,6 +41,14 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null; } + /// + /// Re-establishes invariants after deserialization, since deserialization bypasses the + /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState(); + /// /// Gets the SQL breakdown as a string for Snowflake. /// @@ -145,7 +154,7 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown // Check if it's a DELETE statement var sqlTrimmed = sql.TrimStart(); if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b", - System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with DELETE."; return false; @@ -161,7 +170,7 @@ public class DeleteBreakdown : SqlServerDeleteBreakdown // Snowflake uses simpler DELETE syntax: DELETE FROM table WHERE condition var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql, @"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$", - System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline); + System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!deleteMatch.Success) { diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs index 3e89ef8..eff1562 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/InsertBreakdown.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.Serialization; using Strata.SqlTools.SqlBreakdown.Utilities; using SqlServerInsertBreakdown = Strata.SqlTools.Breakdowns.SqlServer.InsertBreakdown; using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser; @@ -65,6 +66,14 @@ public class InsertBreakdown : SqlServerInsertBreakdown ValuesClause.Clause = string.Join(",", valuesList); } + /// + /// Re-establishes invariants after deserialization, since deserialization bypasses the + /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState(); + #region Parse Methods /// @@ -149,7 +158,7 @@ public class InsertBreakdown : SqlServerInsertBreakdown // Check if it's an INSERT statement var sqlTrimmed = sql.TrimStart(); if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b", - System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with INSERT INTO."; return false; @@ -165,7 +174,7 @@ public class InsertBreakdown : SqlServerInsertBreakdown // Parse INSERT statement using regex var insertMatch = System.Text.RegularExpressions.Regex.Match(sql, @"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)", - System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline); + System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!insertMatch.Success) { diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs index 99056a0..cb2c65b 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/ProcedureBreakdown.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.Serialization; using System.Text; using SqlServerProcedureBreakdown = Strata.SqlTools.Breakdowns.SqlServer.ProcedureBreakdown; using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser; @@ -46,6 +47,15 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown Parameters = parameters ?? new Dictionary(); } + /// + /// Re-establishes invariants after deserialization, since deserialization bypasses the + /// constructors that normally initialize the procedure name and parameter collection + /// (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState(); + /// /// Gets the SQL breakdown as a string for Snowflake. /// @@ -157,7 +167,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown // Check if it's a CALL statement (Snowflake syntax) or EXEC (for compatibility) var sqlTrimmed = sql.TrimStart(); if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*(CALL|EXEC|EXECUTE)\b", - System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with CALL, EXEC, or EXECUTE."; return false; @@ -174,7 +184,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown // Pattern: CALL procedureName(param => value, ...) var callMatch = System.Text.RegularExpressions.Regex.Match(sql, @"(?:CALL|EXEC|EXECUTE)\s+([^\s\(]+)(?:\s*\((.*?)\))?$", - System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline); + System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!callMatch.Success) { @@ -192,7 +202,7 @@ public class ProcedureBreakdown : SqlServerProcedureBreakdown // Parse parameters - Snowflake uses param => value syntax var paramMatches = System.Text.RegularExpressions.Regex.Matches(parametersText, @"(\w+)\s*=>\s*([^,]+)(?:,|$)", - System.Text.RegularExpressions.RegexOptions.IgnoreCase); + System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); foreach (System.Text.RegularExpressions.Match paramMatch in paramMatches) { diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs index bc9fe97..de946fd 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdown.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.Serialization; using System.Text; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Expressions; @@ -85,6 +86,14 @@ public class QueryBreakdown : SqlServerQueryBreakdown OrderByClause.Comment = orderByComments.Count > 0 ? string.Join(" ", orderByComments) : null; } + /// + /// Re-establishes invariants after deserialization, since deserialization bypasses the + /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState(); + /// /// Adds a parameter to the query using Snowflake's :param format. /// Also adds @param format for compatibility. diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdownCollection.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdownCollection.cs index 3e18e92..062f0eb 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdownCollection.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/QueryBreakdownCollection.cs @@ -1,3 +1,4 @@ +using System.Runtime.Serialization; using System.Text; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Interfaces; @@ -36,6 +37,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection _queryBreakdowns = new List(queryBreakdowns ?? Enumerable.Empty()); } + /// + /// Validates that the backing list survived deserialization, since deserialization bypasses + /// the constructors that normally initialize it (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (_queryBreakdowns is null) + { + throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list."); + } + } + /// /// Gets the collection of QueryBreakdown objects. /// @@ -170,7 +185,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection // This avoids false positives from @parameter syntax var stagePattern = @"@[\w~]+/"; - if (!System.Text.RegularExpressions.Regex.IsMatch(sql, stagePattern)) + if (!System.Text.RegularExpressions.Regex.IsMatch(sql, stagePattern, System.Text.RegularExpressions.RegexOptions.None, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { return false; } @@ -184,7 +199,7 @@ public class QueryBreakdownCollection : SqlBreakdownCollection ? $@"@~/{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@', '~', '/'))}/" : $@"@{System.Text.RegularExpressions.Regex.Escape(stageName.TrimStart('@'))}/"; - return System.Text.RegularExpressions.Regex.IsMatch(sql, specificPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase); + return System.Text.RegularExpressions.Regex.IsMatch(sql, specificPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); }); } diff --git a/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs b/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs index a02b190..21e5341 100644 --- a/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs +++ b/src/Strata.SqlTools.Snowflake/Breakdowns/UpdateBreakdown.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.Serialization; using System.Text; using SqlServerUpdateBreakdown = Strata.SqlTools.Breakdowns.SqlServer.UpdateBreakdown; using StatementParser = Strata.SqlTools.Statements.Snowflake.StatementParser; @@ -45,6 +46,14 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown WhereClause.Comment = whereComments.Count > 0 ? string.Join(" ", whereComments) : null; } + /// + /// Re-establishes invariants after deserialization, since deserialization bypasses the + /// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState(); + /// /// Gets the SQL breakdown as a string for Snowflake. /// @@ -160,7 +169,7 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown // Check if it's an UPDATE statement var sqlTrimmed = sql.TrimStart(); if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b", - System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with UPDATE."; return false; @@ -176,7 +185,7 @@ public class UpdateBreakdown : SqlServerUpdateBreakdown // Parse UPDATE statement - handle both with and without FROM clause var updateMatch = System.Text.RegularExpressions.Regex.Match(sql, @"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$", - System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline); + System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!updateMatch.Success) { diff --git a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownBase.cs b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownBase.cs index a04cc67..18252f9 100644 --- a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownBase.cs +++ b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownBase.cs @@ -48,6 +48,18 @@ public abstract class SqlBreakdownBase : ISqlBreakdown /// public bool IsUsingFinishClause => FinishClauses.Count > 0; + /// + /// Re-establishes the invariants normally guaranteed by the constructors after the object + /// is reconstructed by deserialization. Deserialization bypasses constructors, so the + /// collection state must be re-validated to avoid a partially-initialized object + /// (SonarQube rule S5766). + /// + protected void RevalidateBreakdownState() + { + SetupClauses ??= new List(); + FinishClauses ??= new ArrayList(); + } + /// /// Gets the SQL breakdown as a string. Must be implemented by derived classes. /// diff --git a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs index c5f47bc..9ecd6cf 100644 --- a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs +++ b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlBreakdownCollection.cs @@ -1,3 +1,4 @@ +using System.Runtime.Serialization; using System.Text; using Strata.SqlTools.SqlBreakdown.Interfaces; using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine; @@ -35,6 +36,20 @@ public class SqlBreakdownCollection : ICollection _breakdowns = new List(breakdowns ?? Enumerable.Empty()); } + /// + /// Validates that the backing list survived deserialization, since deserialization bypasses + /// the constructors that normally initialize it (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (_breakdowns is null) + { + throw new SerializationException("Deserialized SqlBreakdownCollection is missing its backing list."); + } + } + /// /// Gets the collection of SQL breakdowns. /// diff --git a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlFilter.cs b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlFilter.cs index 2c1b2da..17bea2b 100644 --- a/src/Strata.SqlTools.SqlBreakdown/Classes/SqlFilter.cs +++ b/src/Strata.SqlTools.SqlBreakdown/Classes/SqlFilter.cs @@ -1,3 +1,4 @@ +using System.Runtime.Serialization; using System.Text; using Strata.SqlTools.SqlBreakdown.Interfaces.Core; @@ -58,6 +59,21 @@ public class SqlFilter : ISqlAppendable } } + /// + /// Validates that the expression and parameter state survived deserialization, since + /// deserialization bypasses the constructors that normally initialize them and enforce the + /// even parameter-name/value pairing (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (_sqlExpression is null || _parameterValues is null) + { + throw new SerializationException("Deserialized SqlFilter is missing its expression or parameter state."); + } + } + /// /// Gets or sets the SQL expression. /// diff --git a/src/Strata.SqlTools.SqlBreakdown/Extensions/StringBuilderEx.cs b/src/Strata.SqlTools.SqlBreakdown/Extensions/StringBuilderEx.cs index 0eb8678..a8fd6f6 100644 --- a/src/Strata.SqlTools.SqlBreakdown/Extensions/StringBuilderEx.cs +++ b/src/Strata.SqlTools.SqlBreakdown/Extensions/StringBuilderEx.cs @@ -1,5 +1,6 @@ using System.Text; using System.Text.RegularExpressions; +using Strata.SqlTools.SqlBreakdown.Utilities; namespace Strata.SqlTools.SqlBreakdown.Extensions; @@ -10,7 +11,7 @@ public class StringBuilderEx { private static readonly Regex AppendFormatExRegex = new Regex( @"\{(?.*?)(?!.*?)?\}", - RegexOptions.Compiled | RegexOptions.IgnoreCase); + RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout); private readonly StringBuilder _innerStringBuilder; diff --git a/src/Strata.SqlTools.SqlBreakdown/Extensions/StringExtensions.cs b/src/Strata.SqlTools.SqlBreakdown/Extensions/StringExtensions.cs index 90550e9..602273a 100644 --- a/src/Strata.SqlTools.SqlBreakdown/Extensions/StringExtensions.cs +++ b/src/Strata.SqlTools.SqlBreakdown/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using Strata.SqlTools.SqlBreakdown.Utilities; namespace Strata.SqlTools.SqlBreakdown.Extensions; @@ -119,7 +120,7 @@ public static class StringExtensions public static bool IsGUID(this string aString) { const string pattern = "^[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}$"; - var match = Regex.Match(aString, pattern); + var match = Regex.Match(aString, pattern, RegexOptions.None, RegexDefaults.MatchTimeout); return match.Success; } diff --git a/src/Strata.SqlTools.SqlBreakdown/Utilities/GuidUtils.cs b/src/Strata.SqlTools.SqlBreakdown/Utilities/GuidUtils.cs index d847f43..a869cd0 100644 --- a/src/Strata.SqlTools.SqlBreakdown/Utilities/GuidUtils.cs +++ b/src/Strata.SqlTools.SqlBreakdown/Utilities/GuidUtils.cs @@ -226,7 +226,7 @@ public static class GuidUtils private static readonly Regex FindFirstGuidRegex = new Regex( "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", - RegexOptions.IgnoreCase | RegexOptions.Compiled); + RegexOptions.IgnoreCase | RegexOptions.Compiled, RegexDefaults.MatchTimeout); #endregion @@ -292,7 +292,7 @@ public static class GuidUtils /// A list of GUID strings found in the input. public static List GetGuids(string value) { - MatchCollection matches = Regex.Matches(value, GUID_STRING); + MatchCollection matches = Regex.Matches(value, GUID_STRING, RegexOptions.None, RegexDefaults.MatchTimeout); return matches.Cast().Select(x => x.Value).ToList(); } diff --git a/src/Strata.SqlTools.SqlBreakdown/Utilities/RegexDefaults.cs b/src/Strata.SqlTools.SqlBreakdown/Utilities/RegexDefaults.cs new file mode 100644 index 0000000..879feb5 --- /dev/null +++ b/src/Strata.SqlTools.SqlBreakdown/Utilities/RegexDefaults.cs @@ -0,0 +1,14 @@ +namespace Strata.SqlTools.SqlBreakdown.Utilities; + +/// +/// Shared defaults for usage across the SQL tools. +/// +public static class RegexDefaults +{ + /// + /// Default match timeout applied to regular expressions to guard against catastrophic + /// backtracking / ReDoS denial-of-service attacks (SonarQube rule S6444). Regular expressions + /// in this library process arbitrary SQL text, so every pattern is given a bounded execution time. + /// + public static readonly TimeSpan MatchTimeout = TimeSpan.FromSeconds(1); +} diff --git a/src/Strata.SqlTools.SqlBreakdown/Utilities/SqlUtils.cs b/src/Strata.SqlTools.SqlBreakdown/Utilities/SqlUtils.cs index 8264275..ec6d6ee 100644 --- a/src/Strata.SqlTools.SqlBreakdown/Utilities/SqlUtils.cs +++ b/src/Strata.SqlTools.SqlBreakdown/Utilities/SqlUtils.cs @@ -115,7 +115,7 @@ public static partial class SqlUtils public static string GetSqlFriendlyName(string str, string replacement = "") { // Invalid characters --> . , ; ' ` : / \ * | ? " & % $ ! + = ( ) [ ] { } - ~ ^ - return new Regex(@"[\.,;'`:/\\*\|?""&%\$!\+=\(\)\[\]{}\-~\ \^]").Replace(str, replacement); + return new Regex(@"[\.,;'`:/\\*\|?""&%\$!\+=\(\)\[\]{}\-~\ \^]", RegexOptions.None, RegexDefaults.MatchTimeout).Replace(str, replacement); } #endregion @@ -382,37 +382,37 @@ public static partial class SqlUtils { string msg = ex.Message; - Match match = Regex.Match(msg, @".*?Violation of UNIQUE KEY constraint (.*?) Cannot insert duplicate key in object (.*?)"); + Match match = Regex.Match(msg, @".*?Violation of UNIQUE KEY constraint (.*?) Cannot insert duplicate key in object (.*?)", RegexOptions.None, RegexDefaults.MatchTimeout); if (match.Success) { return WellKnownSqlError.UniqueKeyViolation; } - match = Regex.Match(msg, @".*Cannot insert duplicate key row in object.*", RegexOptions.IgnoreCase); + match = Regex.Match(msg, @".*Cannot insert duplicate key row in object.*", RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout); if (match.Success) { return WellKnownSqlError.UniqueKeyViolation; } - match = Regex.Match(msg, @".*Update or insert of view or function (.*?) failed because it contains a derived or constant field.*"); + match = Regex.Match(msg, @".*Update or insert of view or function (.*?) failed because it contains a derived or constant field.*", RegexOptions.None, RegexDefaults.MatchTimeout); if (match.Success) { return WellKnownSqlError.UpdateViewMultipleTables; } - match = Regex.Match(msg, @".*There is already an object named (.*?) in the database.*"); + match = Regex.Match(msg, @".*There is already an object named (.*?) in the database.*", RegexOptions.None, RegexDefaults.MatchTimeout); if (match.Success) { return WellKnownSqlError.ObjectAlreadyExists; } - match = Regex.Match(msg, @".*Cannot truncate table (.*?) because it is being referenced by a FOREIGN KEY constraint.*"); + match = Regex.Match(msg, @".*Cannot truncate table (.*?) because it is being referenced by a FOREIGN KEY constraint.*", RegexOptions.None, RegexDefaults.MatchTimeout); if (match.Success) { return WellKnownSqlError.TruncateTableForeignKeyReferenceError; } - match = Regex.Match(msg, @".*Could not truncate object (.*?) because it is not a table.*"); + match = Regex.Match(msg, @".*Could not truncate object (.*?) because it is not a table.*", RegexOptions.None, RegexDefaults.MatchTimeout); if (match.Success) { return WellKnownSqlError.TruncateTableNotATableError; diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs index 96e77db..fd8c44b 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/DeleteBreakdown.cs @@ -145,7 +145,7 @@ public class DeleteBreakdown : SqlBreakdownBase // Check if it's a DELETE statement var sqlTrimmed = sql.TrimStart(); if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*DELETE\b", - System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with DELETE."; return false; @@ -162,14 +162,14 @@ public class DeleteBreakdown : SqlBreakdownBase // Pattern: DELETE [table_alias] FROM table WHERE condition var deleteMatch = System.Text.RegularExpressions.Regex.Match(sql, @"DELETE\s+(.*?)\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$", - System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline); + System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!deleteMatch.Success) { // Try simpler pattern: DELETE FROM table WHERE condition deleteMatch = System.Text.RegularExpressions.Regex.Match(sql, @"DELETE\s+FROM\s+(.*?)(?:\s+WHERE\s+(.*))?$", - System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline); + System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!deleteMatch.Success) { diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs index e8268b6..071e7d0 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/InsertBreakdown.cs @@ -158,7 +158,7 @@ public class InsertBreakdown : SqlBreakdownBase // Check if it's an INSERT statement var sqlTrimmed = sql.TrimStart(); if (!System.Text.RegularExpressions.Regex.IsMatch(sqlTrimmed, @"^\s*INSERT\s+INTO\b", - System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with INSERT INTO."; return false; @@ -175,7 +175,7 @@ public class InsertBreakdown : SqlBreakdownBase // Pattern: INSERT INTO table (columns) VALUES (values) var insertMatch = System.Text.RegularExpressions.Regex.Match(sql, @"INSERT\s+INTO\s+([^\(\s]+)\s*\(([^\)]*)\)\s*VALUES\s*\(([^\)]*)\)", - System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline); + System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!insertMatch.Success) { diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs index 84bb93b..cf697a4 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/ProcedureBreakdown.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.Serialization; using System.Text; using System.Text.RegularExpressions; using Strata.SqlTools.SqlBreakdown.Classes; @@ -45,6 +46,20 @@ public class ProcedureBreakdown : SqlBreakdownBase Parameters = parameters ?? new Dictionary(); } + /// + /// Re-establishes invariants after deserialization, since deserialization bypasses the + /// constructors that normally initialize the procedure name and parameter collection + /// (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + RevalidateBreakdownState(); + ProcedureName ??= new SqlClause(); + Parameters ??= new Dictionary(); + } + /// /// Gets or sets the stored procedure name. /// @@ -169,7 +184,7 @@ public class ProcedureBreakdown : SqlBreakdownBase // Check if it's an EXEC or EXECUTE statement var sqlTrimmed = sql.TrimStart(); if (!Regex.IsMatch(sqlTrimmed, @"^\s*(EXEC|EXECUTE)\b", - RegexOptions.IgnoreCase)) + RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with EXEC or EXECUTE."; return false; @@ -186,7 +201,7 @@ public class ProcedureBreakdown : SqlBreakdownBase // Pattern: EXEC[UTE] procedureName [@param = value, ...] var execMatch = Regex.Match(sql, @"(?:EXEC|EXECUTE)\s+([^\s@,]+)(?:\s+(.*))?$", - RegexOptions.IgnoreCase | RegexOptions.Singleline); + RegexOptions.IgnoreCase | RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!execMatch.Success) { @@ -204,7 +219,7 @@ public class ProcedureBreakdown : SqlBreakdownBase // Parse parameters - handle both @param = value and positional parameters var paramMatches = Regex.Matches(parametersText, @"(@\w+)\s*=\s*([^,]+)(?:,|$)", - RegexOptions.IgnoreCase); + RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); parameters = paramMatches .Cast() diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs index 2395103..e817b91 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdown.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Runtime.Serialization; using System.Text; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Expressions; @@ -107,6 +108,28 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown } } + /// + /// Re-establishes invariants after deserialization, since deserialization bypasses the + /// constructors that normally initialize the parameter, WITH-clause, and clause backing + /// fields (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + RevalidateBreakdownState(); + + _parameterList ??= new List(); + _withClauses ??= new List(); + _selectClause ??= new SqlExpressionClause(splitOnComma: true); + _fromClause ??= new SqlClause(); + _whereClause ??= new SqlExpressionClause(splitOnComma: false); + _groupByClause ??= new SqlExpressionClause(splitOnComma: true); + _havingClause ??= new SqlExpressionClause(splitOnComma: false); + _orderByClause ??= new SqlExpressionClause(splitOnComma: true); + _clausesCacheDirty = true; + } + #region Properties /// @@ -1308,7 +1331,7 @@ public class QueryBreakdown : SqlBreakdownBase, IQueryBreakdown { // Try to extract position from error message var match = System.Text.RegularExpressions.Regex.Match(error, @"position[:\s]+(\d+)", - System.Text.RegularExpressions.RegexOptions.IgnoreCase); + System.Text.RegularExpressions.RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (match.Success && int.TryParse(match.Groups[1].Value, out var parsedPos)) { position = parsedPos; diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdownCollection.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdownCollection.cs index 3cebcb5..0594a9b 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdownCollection.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/QueryBreakdownCollection.cs @@ -1,3 +1,4 @@ +using System.Runtime.Serialization; using System.Text; using Strata.SqlTools.SqlBreakdown.Classes; using Strata.SqlTools.SqlBreakdown.Interfaces; @@ -33,6 +34,20 @@ public class QueryBreakdownCollection : SqlBreakdownCollection _queryBreakdowns = new List(queryBreakdowns ?? Enumerable.Empty()); } + /// + /// Validates that the backing list survived deserialization, since deserialization bypasses + /// the constructors that normally initialize it (SonarQube rule S5766). + /// + /// The streaming context for the deserialization operation. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + if (_queryBreakdowns is null) + { + throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list."); + } + } + /// /// Gets the collection of QueryBreakdown objects. /// diff --git a/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs b/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs index 3acfe1f..f5ee6b6 100644 --- a/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs +++ b/src/Strata.SqlTools.SqlServer/Breakdowns/UpdateBreakdown.cs @@ -165,7 +165,7 @@ public class UpdateBreakdown : SqlBreakdownBase // Check if it's an UPDATE statement var sqlTrimmed = sql.TrimStart(); if (!Regex.IsMatch(sqlTrimmed, @"^\s*UPDATE\b", - RegexOptions.IgnoreCase)) + RegexOptions.IgnoreCase, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout)) { errorMessage = "SQL statement must start with UPDATE."; return false; @@ -182,7 +182,7 @@ public class UpdateBreakdown : SqlBreakdownBase // Pattern: UPDATE table SET column=value [FROM table] [WHERE condition] var updateMatch = Regex.Match(sql, @"UPDATE\s+([^\s]+)\s+SET\s+(.*?)(?:\s+FROM\s+(.*?))?(?:\s+WHERE\s+(.*))?$", - RegexOptions.IgnoreCase | RegexOptions.Singleline); + RegexOptions.IgnoreCase | RegexOptions.Singleline, Strata.SqlTools.SqlBreakdown.Utilities.RegexDefaults.MatchTimeout); if (!updateMatch.Success) { diff --git a/src/Strata.SqlTools.SqlServer/Statements/StatementParser.cs b/src/Strata.SqlTools.SqlServer/Statements/StatementParser.cs index 3795ede..c2b7d5c 100644 --- a/src/Strata.SqlTools.SqlServer/Statements/StatementParser.cs +++ b/src/Strata.SqlTools.SqlServer/Statements/StatementParser.cs @@ -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 /// The regex pattern to match parameter names. protected virtual void ExtractParameters(Dictionary 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() .Select(match => match.Value)