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:
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string for Snowflake.
|
||||
/// </summary>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
|
||||
|
||||
#region Parse Methods
|
||||
|
||||
/// <summary>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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<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();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string for Snowflake.
|
||||
/// </summary>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a parameter to the query using Snowflake's :param format.
|
||||
/// Also adds @param format for compatibility.
|
||||
|
||||
@@ -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<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>
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-establishes invariants after deserialization, since deserialization bypasses the
|
||||
/// constructors that normally initialize the breakdown's clause state (SonarQube rule S5766).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[OnDeserialized]
|
||||
private void OnDeserialized(StreamingContext context) => RevalidateBreakdownState();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string for Snowflake.
|
||||
/// </summary>
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user