diff --git a/src/modules/Elsa.Sql.MySql/MySqlClient.cs b/src/modules/Elsa.Sql.MySql/MySqlClient.cs index 60d84fd3e..f8a17c61e 100644 --- a/src/modules/Elsa.Sql.MySql/MySqlClient.cs +++ b/src/modules/Elsa.Sql.MySql/MySqlClient.cs @@ -1,6 +1,7 @@ using MySql.Data.MySqlClient; using Elsa.Sql.Client; using System.Data; +using Elsa.Sql.Models; namespace Elsa.Sql.MySql; @@ -9,7 +10,7 @@ public class MySqlClient : BaseSqlClient, ISqlClient private string? _connectionString; /// - /// MySql client implimentation. + /// MySql client implementation. /// /// public MySqlClient(string? connectionString) => _connectionString = connectionString; @@ -17,11 +18,12 @@ public class MySqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteCommandAsync(string sqlCommand) + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) { using var connection = new MySqlConnection(_connectionString); connection.Open(); - var command = new MySqlCommand(sqlCommand, connection); + var command = new MySqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -30,11 +32,12 @@ public class MySqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteScalarAsync(string sqlQuery) + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) { using var connection = new MySqlConnection(_connectionString); connection.Open(); - var command = new MySqlCommand(sqlQuery, connection); + var command = new MySqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -43,13 +46,29 @@ public class MySqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteQueryAsync(string sqlQuery) + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) { using var connection = new MySqlConnection(_connectionString); connection.Open(); - var command = new MySqlCommand(sqlQuery, connection); + var command = new MySqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private MySqlCommand AddParameters(MySqlCommand command, Dictionary parameters) + { + foreach (var param in parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); + } + return command; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs index 495439f30..48d54101a 100644 --- a/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs +++ b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs @@ -1,6 +1,7 @@ using Npgsql; using Elsa.Sql.Client; using System.Data; +using Elsa.Sql.Models; namespace Elsa.Sql.PostgreSql; @@ -9,7 +10,7 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient private string? _connectionString; /// - /// PostgreSQL client implimentation. + /// PostgreSQL client implementation. /// /// public PostgreSqlClient(string? connectionString) => _connectionString = connectionString; @@ -17,11 +18,12 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteCommandAsync(string sqlCommand) + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) { using var connection = new NpgsqlConnection(_connectionString); connection.Open(); - var command = new NpgsqlCommand(sqlCommand, connection); + var command = new NpgsqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -30,11 +32,12 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteScalarAsync(string sqlQuery) + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) { using var connection = new NpgsqlConnection(_connectionString); connection.Open(); - var command = new NpgsqlCommand(sqlQuery, connection); + var command = new NpgsqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -43,13 +46,29 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteQueryAsync(string sqlQuery) + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) { using var connection = new NpgsqlConnection(_connectionString); connection.Open(); - var command = new NpgsqlCommand(sqlQuery, connection); + var command = new NpgsqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private NpgsqlCommand AddParameters(NpgsqlCommand command, Dictionary parameters) + { + foreach (var param in parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); + } + return command; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs index f5b57c263..338d9f849 100644 --- a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs +++ b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs @@ -1,5 +1,6 @@ using System.Data; using Elsa.Sql.Client; +using Elsa.Sql.Models; using Microsoft.Data.SqlClient; namespace Elsa.Sql.SqlServer; @@ -9,7 +10,7 @@ public class SqlServerClient : BaseSqlClient, ISqlClient private string? _connectionString; /// - /// Microsoft SQL server client implimentation. + /// Microsoft SQL server client implementation. /// /// public SqlServerClient(string? connectionString) => _connectionString = connectionString; @@ -17,11 +18,12 @@ public class SqlServerClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteCommandAsync(string sqlCommand) + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqlConnection(_connectionString); connection.Open(); - var command = new SqlCommand(sqlCommand, connection); + var command = new SqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -30,11 +32,12 @@ public class SqlServerClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteScalarAsync(string sqlQuery) + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqlConnection(_connectionString); connection.Open(); - var command = new SqlCommand(sqlQuery, connection); + var command = new SqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -43,13 +46,29 @@ public class SqlServerClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteQueryAsync(string sqlQuery) + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqlConnection(_connectionString); connection.Open(); - var command = new SqlCommand(sqlQuery, connection); + var command = new SqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private SqlCommand AddParameters(SqlCommand command, Dictionary parameters) + { + foreach (var param in parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); + } + return command; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs index d67e69342..61caf6ef5 100644 --- a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs +++ b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs @@ -1,5 +1,6 @@ using System.Data; using Elsa.Sql.Client; +using Elsa.Sql.Models; using Microsoft.Data.Sqlite; namespace Elsa.Sql.Sqlite; @@ -9,7 +10,7 @@ public class SqliteClient : BaseSqlClient, ISqlClient private string? _connectionString; /// - /// Sqlite client implimentation. + /// Sqlite client implementation. /// /// public SqliteClient(string? connectionString) => _connectionString = connectionString; @@ -17,11 +18,12 @@ public class SqliteClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteCommandAsync(string sqlCommand) + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqliteConnection(_connectionString); connection.Open(); - var command = new SqliteCommand(sqlCommand, connection); + var command = new SqliteCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -30,11 +32,12 @@ public class SqliteClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteScalarAsync(string sqlQuery) + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqliteConnection(_connectionString); connection.Open(); - var command = new SqliteCommand(sqlQuery, connection); + var command = new SqliteCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -43,13 +46,29 @@ public class SqliteClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteQueryAsync(string sqlQuery) + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqliteConnection(_connectionString); connection.Open(); - var command = new SqliteCommand(sqlQuery, connection); + var command = new SqliteCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private SqliteCommand AddParameters(SqliteCommand command, Dictionary parameters) + { + foreach (var param in parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); + } + return command; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Client/ISqlClient.cs b/src/modules/Elsa.Sql/Client/ISqlClient.cs index 2312097b8..e5b743b3b 100644 --- a/src/modules/Elsa.Sql/Client/ISqlClient.cs +++ b/src/modules/Elsa.Sql/Client/ISqlClient.cs @@ -1,4 +1,5 @@ using System.Data; +using Elsa.Sql.Models; namespace Elsa.Sql.Client; @@ -7,21 +8,21 @@ public interface ISqlClient /// /// Asynchronously executes a Transact-SQL statement against the connection and returns the number of rows affected. /// - /// The command to execute + /// The evaluated query to execute. /// The number of rows affected. - public Task ExecuteCommandAsync(string sqlCommand); + public Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery); /// /// Asynchronously executes the query, and returns the first column of the first row in the result set returned by the query. Additional columns or rows are ignored. /// - /// The query to execute + /// The evaluated query to execute. /// The first column of the first row in the result set, or a null reference if the result set is empty. Returns a maximum of 2033 characters. - public Task ExecuteScalarAsync(string sqlQuery); + public Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery); /// /// Asynchronously executes the query, and returns a dataset of data returned by the query. /// - /// Query to execute + /// The evaluated query to execute. /// DataSet of the queried data - public Task ExecuteQueryAsync(string sqlQuery); + public Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery); } \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Contracts/ISqlEvaluator.cs b/src/modules/Elsa.Sql/Contracts/ISqlEvaluator.cs index 512d9fbd9..0817f409b 100644 --- a/src/modules/Elsa.Sql/Contracts/ISqlEvaluator.cs +++ b/src/modules/Elsa.Sql/Contracts/ISqlEvaluator.cs @@ -1,4 +1,5 @@ using Elsa.Expressions.Models; +using Elsa.Sql.Models; using JetBrains.Annotations; namespace Elsa.Sql.Contracts; @@ -16,8 +17,8 @@ public interface ISqlEvaluator /// The context in which the expression is evaluated. /// A set of options. /// An optional cancellation token. - /// The result of the evaluation. - Task EvaluateAsync( + /// The result. + Task EvaluateAsync( string expression, ExpressionExecutionContext context, ExpressionEvaluatorOptions options, diff --git a/src/modules/Elsa.Sql/Elsa.Sql.csproj b/src/modules/Elsa.Sql/Elsa.Sql.csproj index bfb24be1e..670705651 100644 --- a/src/modules/Elsa.Sql/Elsa.Sql.csproj +++ b/src/modules/Elsa.Sql/Elsa.Sql.csproj @@ -9,6 +9,7 @@ + diff --git a/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs b/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs new file mode 100644 index 000000000..36aae3131 --- /dev/null +++ b/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs @@ -0,0 +1,35 @@ +namespace Elsa.Sql.Models +{ + /// + /// Represents a safely formatted SQL expression result. + /// + public class EvaluatedQuery + { + /// + /// Query with parameterized values + /// + public string Query { get; } + + /// + /// Parameters to inject into the query at execution + /// + public Dictionary Parameters { get; } = new Dictionary(); + + /// + /// An evaluated query response. + /// + /// The evaluated query + public EvaluatedQuery(string query) => Query = query; + + /// + /// An evaluated query response. + /// + /// The evaluated query + /// Parameters to pass into the parameterized query + public EvaluatedQuery(string query, Dictionary parameters) + { + Query = query; + Parameters = parameters; + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs index 88bde44d4..e8a06998d 100644 --- a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs +++ b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs @@ -2,6 +2,7 @@ using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.Sql.Contracts; +using Elsa.Sql.Models; namespace Elsa.Sql.Services; @@ -14,16 +15,18 @@ namespace Elsa.Sql.Services; public class SqlEvaluator() : ISqlEvaluator { /// - public async Task EvaluateAsync( + public async Task EvaluateAsync( string expression, ExpressionExecutionContext context, ExpressionEvaluatorOptions options, CancellationToken cancellationToken = default) { - if (!expression.Contains("@")) return expression; + if (!expression.Contains("@")) return new EvaluatedQuery(expression); var sb = new StringBuilder(); + var parameters = new Dictionary(); int start = 0; + int paramIndex = 0; while (start < expression.Length) { @@ -46,11 +49,14 @@ public class SqlEvaluator() : ISqlEvaluator object? value = ResolveValue(key, context); if (value is null) throw new NullReferenceException($"No value found for '{key}'."); - sb.Append(value?.ToString() ?? $"<{key}>"); + string paramName = $"@param{paramIndex++}"; + parameters[paramName] = value; + + sb.Append(paramName); start = endIndex; } - return sb.ToString(); + return new EvaluatedQuery(sb.ToString(), parameters); } private object? ResolveValue(string key, ExpressionExecutionContext context)