Adds SQL injection prevention with the use of parameters in the SqlEvaluator.

This commit is contained in:
Matt 2025-03-15 01:26:11 +00:00
parent 62da3c8618
commit a4e6e75e55
9 changed files with 160 additions and 40 deletions

View file

@ -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;
/// <summary>
/// MySql client implimentation.
/// MySql client implementation.
/// </summary>
/// <param name="connectionString"></param>
public MySqlClient(string? connectionString) => _connectionString = connectionString;
@ -17,11 +18,12 @@ public class MySqlClient : BaseSqlClient, ISqlClient
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<int?> ExecuteCommandAsync(string sqlCommand)
public async Task<int?> 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
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<object?> ExecuteScalarAsync(string sqlQuery)
public async Task<object?> 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
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<DataSet?> ExecuteQueryAsync(string sqlQuery)
public async Task<DataSet?> 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));
}
/// <summary>
/// Inject parameters into the query to prevent SQL injection.
/// </summary>
/// <param name="command">Command to add the parameters to</param>
/// <param name="parameters">Parameters to add</param>
/// <returns></returns>
private MySqlCommand AddParameters(MySqlCommand command, Dictionary<string, object?> parameters)
{
foreach (var param in parameters)
{
command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value);
}
return command;
}
}

View file

@ -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;
/// <summary>
/// PostgreSQL client implimentation.
/// PostgreSQL client implementation.
/// </summary>
/// <param name="connectionString"></param>
public PostgreSqlClient(string? connectionString) => _connectionString = connectionString;
@ -17,11 +18,12 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<int?> ExecuteCommandAsync(string sqlCommand)
public async Task<int?> 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
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<object?> ExecuteScalarAsync(string sqlQuery)
public async Task<object?> 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
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<DataSet?> ExecuteQueryAsync(string sqlQuery)
public async Task<DataSet?> 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));
}
/// <summary>
/// Inject parameters into the query to prevent SQL injection.
/// </summary>
/// <param name="command">Command to add the parameters to</param>
/// <param name="parameters">Parameters to add</param>
/// <returns></returns>
private NpgsqlCommand AddParameters(NpgsqlCommand command, Dictionary<string, object?> parameters)
{
foreach (var param in parameters)
{
command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value);
}
return command;
}
}

View file

@ -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;
/// <summary>
/// Microsoft SQL server client implimentation.
/// Microsoft SQL server client implementation.
/// </summary>
/// <param name="connectionString"></param>
public SqlServerClient(string? connectionString) => _connectionString = connectionString;
@ -17,11 +18,12 @@ public class SqlServerClient : BaseSqlClient, ISqlClient
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<int?> ExecuteCommandAsync(string sqlCommand)
public async Task<int?> 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
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<object?> ExecuteScalarAsync(string sqlQuery)
public async Task<object?> 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
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<DataSet?> ExecuteQueryAsync(string sqlQuery)
public async Task<DataSet?> 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));
}
/// <summary>
/// Inject parameters into the query to prevent SQL injection.
/// </summary>
/// <param name="command">Command to add the parameters to</param>
/// <param name="parameters">Parameters to add</param>
/// <returns></returns>
private SqlCommand AddParameters(SqlCommand command, Dictionary<string, object?> parameters)
{
foreach (var param in parameters)
{
command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value);
}
return command;
}
}

View file

@ -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;
/// <summary>
/// Sqlite client implimentation.
/// Sqlite client implementation.
/// </summary>
/// <param name="connectionString"></param>
public SqliteClient(string? connectionString) => _connectionString = connectionString;
@ -17,11 +18,12 @@ public class SqliteClient : BaseSqlClient, ISqlClient
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<int?> ExecuteCommandAsync(string sqlCommand)
public async Task<int?> 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
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<object?> ExecuteScalarAsync(string sqlQuery)
public async Task<object?> 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
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<DataSet?> ExecuteQueryAsync(string sqlQuery)
public async Task<DataSet?> 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));
}
/// <summary>
/// Inject parameters into the query to prevent SQL injection.
/// </summary>
/// <param name="command">Command to add the parameters to</param>
/// <param name="parameters">Parameters to add</param>
/// <returns></returns>
private SqliteCommand AddParameters(SqliteCommand command, Dictionary<string, object?> parameters)
{
foreach (var param in parameters)
{
command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value);
}
return command;
}
}

View file

@ -1,4 +1,5 @@
using System.Data;
using Elsa.Sql.Models;
namespace Elsa.Sql.Client;
@ -7,21 +8,21 @@ public interface ISqlClient
/// <summary>
/// Asynchronously executes a Transact-SQL statement against the connection and returns the number of rows affected.
/// </summary>
/// <param name="sqlCommand">The command to execute</param>
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
/// <returns>The number of rows affected.</returns>
public Task<int?> ExecuteCommandAsync(string sqlCommand);
public Task<int?> ExecuteCommandAsync(EvaluatedQuery evaluatedQuery);
/// <summary>
/// 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.
/// </summary>
/// <param name="sqlQuery">The query to execute</param>
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
/// <returns>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.</returns>
public Task<object?> ExecuteScalarAsync(string sqlQuery);
public Task<object?> ExecuteScalarAsync(EvaluatedQuery evaluatedQuery);
/// <summary>
/// Asynchronously executes the query, and returns a dataset of data returned by the query.
/// </summary>
/// <param name="sqlQuery">Query to execute</param>
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
/// <returns>DataSet of the queried data</returns>
public Task<DataSet?> ExecuteQueryAsync(string sqlQuery);
public Task<DataSet?> ExecuteQueryAsync(EvaluatedQuery evaluatedQuery);
}

View file

@ -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
/// <param name="context">The context in which the expression is evaluated.</param>
/// <param name="options">A set of options.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The result of the evaluation.</returns>
Task<string?> EvaluateAsync(
/// <returns>The <see cref="EvaluatedQuery"/> result.</returns>
Task<EvaluatedQuery> EvaluateAsync(
string expression,
ExpressionExecutionContext context,
ExpressionEvaluatorOptions options,

View file

@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
<ProjectReference Include="..\Elsa.Sql.Models\Elsa.Sql.Models.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>

View file

@ -0,0 +1,35 @@
namespace Elsa.Sql.Models
{
/// <summary>
/// Represents a safely formatted SQL expression result.
/// </summary>
public class EvaluatedQuery
{
/// <summary>
/// Query with parameterized values
/// </summary>
public string Query { get; }
/// <summary>
/// Parameters to inject into the query at execution
/// </summary>
public Dictionary<string, object?> Parameters { get; } = new Dictionary<string, object?>();
/// <summary>
/// An evaluated query response.
/// </summary>
/// <param name="query">The evaluated query</param>
public EvaluatedQuery(string query) => Query = query;
/// <summary>
/// An evaluated query response.
/// </summary>
/// <param name="query">The evaluated query</param>
/// <param name="parameters">Parameters to pass into the parameterized query</param>
public EvaluatedQuery(string query, Dictionary<string, object?> parameters)
{
Query = query;
Parameters = parameters;
}
}
}

View file

@ -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
{
/// <inheritdoc />
public async Task<string?> EvaluateAsync(
public async Task<EvaluatedQuery> 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<string, object?>();
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)