Adds SQL injection prevention with the use of parameters in the SqlEvaluator.
This commit is contained in:
parent
62da3c8618
commit
a4e6e75e55
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
35
src/modules/Elsa.Sql/Models/EvaluatedQuery.cs
Normal file
35
src/modules/Elsa.Sql/Models/EvaluatedQuery.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue