fix(security): Resolve SonarQube security hotspots
SonarQube Analysis / sonarqube (pull_request) Successful in 3m9s
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:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the procedure name and parameter collection
|
||||
/// (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
RevalidateBreakdownState();
|
||||
ProcedureName ??= new SqlClause();
|
||||
Parameters ??= new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stored procedure name.
|
||||
/// </summary>
|
||||
@@ -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<System.Text.RegularExpressions.Match>()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the parameter, WITH-clause, and clause backing
|
||||
/// fields (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
RevalidateBreakdownState();
|
||||
|
||||
_parameterList ??= new List<IQueryParam>();
|
||||
_withClauses ??= new List<IWithClause>();
|
||||
_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
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
|
||||
@@ -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<QueryBreakdown>(queryBreakdowns ?? Enumerable.Empty<QueryBreakdown>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the backing list survived deserialization, since deserialization bypasses
|
||||
/// the constructors that normally initialize it (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context)
|
||||
{
|
||||
if (_queryBreakdowns is null)
|
||||
{
|
||||
throw new SerializationException("Deserialized QueryBreakdownCollection is missing its backing list.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of QueryBreakdown objects.
|
||||
/// </summary>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user