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:
@@ -48,6 +48,18 @@ public abstract class SqlBreakdownBase : ISqlBreakdown
|
||||
/// </summary>
|
||||
public bool IsUsingFinishClause => FinishClauses.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
protected void RevalidateBreakdownState()
|
||||
{
|
||||
SetupClauses ??= new List<string>();
|
||||
FinishClauses ??= new ArrayList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL breakdown as a string. Must be implemented by derived classes.
|
||||
/// </summary>
|
||||
|
||||
@@ -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<ISqlBreakdown>
|
||||
_breakdowns = new List<ISqlBreakdown>(breakdowns ?? Enumerable.Empty<ISqlBreakdown>());
|
||||
}
|
||||
|
||||
/// <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 (_breakdowns is null)
|
||||
{
|
||||
throw new SerializationException("Deserialized SqlBreakdownCollection is missing its backing list.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of SQL breakdowns.
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <param name="context">The streaming context for the deserialization operation.</param>
|
||||
[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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL expression.
|
||||
/// </summary>
|
||||
|
||||
@@ -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(
|
||||
@"\{(?<Index>.*?)(?<Comment>!.*?)?\}",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase, RegexDefaults.MatchTimeout);
|
||||
|
||||
private readonly StringBuilder _innerStringBuilder;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <returns>A list of GUID strings found in the input.</returns>
|
||||
public static List<string> GetGuids(string value)
|
||||
{
|
||||
MatchCollection matches = Regex.Matches(value, GUID_STRING);
|
||||
MatchCollection matches = Regex.Matches(value, GUID_STRING, RegexOptions.None, RegexDefaults.MatchTimeout);
|
||||
return matches.Cast<Match>().Select(x => x.Value).ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Strata.SqlTools.SqlBreakdown.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Shared defaults for <see cref="System.Text.RegularExpressions.Regex"/> usage across the SQL tools.
|
||||
/// </summary>
|
||||
public static class RegexDefaults
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static readonly TimeSpan MatchTimeout = TimeSpan.FromSeconds(1);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user