PostgreSQL · Delete
Catch an SQL injection attempt on delete
Catching an SQL injection attempt on delete, in four scenarios. A table or match column that carries SQL is refused with ARX1017 SuspectedInjection before the database. SQL in the value to delete by, the classic ' OR '1'='1 that would empty a table when concatenated, is a parameter: it deletes nothing.
Set up for this page
The scenarios build on the sample application: SampleContext.cs (the dbContext every sample is given); SampleOutput.cs (Step and Show, which print); SampleContext.Application.cs (dbContext.Service, the application's ARX service); SampleCatalog.cs (the tables' ids, keys, lookups and display names).
Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:
var accounts = new List<ArxEntity>();
foreach (var number in new[] { "ACC-I301", "ACC-I302", "ACC-I303" })
{
var account = new ArxEntity("account");
account["name"] = "Injection: keep me";
account["account_number"] = number;
accounts.Add(account);
}
var ids = await service.CreateMultipleAsync(accounts);1 A table name from the request carries SQL
DELETE /api/{type}/{id} with the type used as the table: the SQL in it is refused before the database.
Already in scope: service (ArxPgDbService), id (ArxRecordId)
var type = "account WHERE 1=1; --";
try
{
await service.DeleteAsync(type, id);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
SecurityLog(error);
}Defined once in this sample.
private static void SecurityLog(ArxValidationException error) => Console.WriteLine($"SECURITY: {error.Message}");Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-I301', 'ACC-I302', 'ACC-I303');Expect: 3: nothing was deleted.
2 SQL in the value to delete by deletes nothing
The classic: "delete the account with this number", and the user types ACC-I301' OR '1'='1. Concatenated into SQL it would delete every account; as a parameter it is an account number nobody has.
Already in scope: service (ArxPgDbService)
var target = new ArxEntity("account");
target["account_number"] = "ACC-I301' OR '1'='1";
int deleted = await service.DeleteAsync(target, new ArxDeleteOptions { Match = ArxMatch.Key("account_number") });
Console.WriteLine($"{deleted} rows deleted");Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-I301', 'ACC-I302', 'ACC-I303');Expect: 3: the text was compared as a value, so it matched no account.
3 The column to delete by, from the request, carries SQL
The column to delete by comes from the request: the SQL in it is refused; only allow columns from a fixed list.
Already in scope: service (ArxPgDbService)
var by = "name = name OR account_number";
var target = new ArxEntity("account");
target[by] = "x";
try
{
await service.DeleteAsync(target, new ArxDeleteOptions { Match = ArxMatch.Columns(by) });
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
SecurityLog(error);
}Defined once in this sample.
private static void SecurityLog(ArxValidationException error) => Console.WriteLine($"SECURITY: {error.Message}");Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*)
FROM account
WHERE name = 'Injection: keep me';Expect: 3.
4 DeleteMultiple with a table name carrying SQL
DeleteMultiple of the three ids with a table name carrying SQL: refused before the database, all three kept.
Already in scope: service (ArxPgDbService), ids (IReadOnlyList<ArxRecordId>)
try
{
await service.DeleteMultipleAsync("account\"; TRUNCATE account; --", ids.ToList());
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
SecurityLog(error);
}Defined once in this sample.
private static void SecurityLog(ArxValidationException error) => Console.WriteLine($"SECURITY: {error.Message}");Check it in the database
Run it in any SQL tool connected to the database, after the code has run.
SELECT count(*)
FROM account
WHERE name = 'Injection: keep me';Expect: 3: every account is still there.
Run the complete sample
The code above comes from one program, built in a fresh project and run against the database. To run it yourself, here is every file and step.
The program, its files and how to run it
Run it locally
- .NET SDK: .NET SDK 10.0.100 or later 10.0.x (target
net10.0). - Create the project:
Terminal dotnet new console -n ArxSample -f net10.0 cd ArxSample - Install the packages:
Terminal dotnet add package ARX.SDK.PostgreSql --version 1.0.0-rc.1 - Database: a PostgreSQL 16 (tested with 16.15) database with the sample schema.
- Configuration: set
ARX_SAMPLES_POSTGRESQLto its connection string. Nothing is hard-coded. - Source code: add the files of the sample application this sample uses (
SampleContext.cs,SampleOutput.cs,SampleContext.Application.cs,SampleCatalog.cs), then replaceProgram.csand add the other file below, with the file names shown. - Run it:
Terminal dotnet run - Expected output: below.
The files
Runs the sample. Generated for this page; it reads the connection string from the environment.
using ArxSamples.PostgreSql;
using ArxSamples.PostgreSql.Operations.Delete.RealCases;
// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_POSTGRESQL")
?? throw new InvalidOperationException("Set the ARX_SAMPLES_POSTGRESQL environment variable to your PostgreSQL connection string.");
await new RealCase5_CatchAnInjectionAttempt().RunAsync(new SampleContext(connectionString));
The sample, as it is in the repository.
using ARX.SDK;
using ARX.SDK.Services;
using static ArxSamples.PostgreSql.SampleOutput;
namespace ArxSamples.PostgreSql.Operations.Delete.RealCases;
/// <summary>
/// Catching an SQL injection attempt on delete, in four scenarios. A table or match column that carries SQL is refused
/// with ARX1017 SuspectedInjection before the database. SQL in the value to delete by, the classic
/// <c>' OR '1'='1</c> that would empty a table when concatenated, is a parameter: it deletes nothing.
/// </summary>
public sealed class RealCase5_CatchAnInjectionAttempt : ISample
{
public string Name => "delete-real-5-catch-an-injection-attempt";
public string Title => "Catch an SQL injection attempt on delete";
public async Task RunAsync(SampleContext dbContext)
{
var accounts = new List<ArxEntity>();
foreach (var number in new[] { "ACC-I301", "ACC-I302", "ACC-I303" })
{
var account = new ArxEntity("account");
account["name"] = "Injection: keep me";
account["account_number"] = number;
accounts.Add(account);
}
var ids = await dbContext.Service.CreateMultipleAsync(accounts);
await Scenario1_ATableNameFromTheRequestAsync(dbContext, ids[0]);
await Scenario2_SqlInTheValueToDeleteByAsync(dbContext);
await Scenario3_TheMatchColumnFromTheRequestAsync(dbContext);
await Scenario4_DeleteMultipleWithABadTableAsync(dbContext, ids);
}
private static void SecurityLog(ArxValidationException error) => Show($"SECURITY: {error.Message}");
/// <summary>1. <c>DELETE /api/{type}/{id}</c> with the type used as the table: the SQL in it is refused before the database.</summary>
private static async Task Scenario1_ATableNameFromTheRequestAsync(SampleContext dbContext, ArxRecordId id)
{
Step("1. A table name from the request carries SQL");
var type = "account WHERE 1=1; --";
try
{
await dbContext.Service.DeleteAsync(type, id);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
SecurityLog(error);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*)
* FROM account
* WHERE account_number IN ('ACC-I301', 'ACC-I302', 'ACC-I303');
* -- Expect: 3: nothing was deleted.
* ===========================================================
*/
}
/// <summary>
/// 2. The classic: "delete the account with this number", and the user types <c>ACC-I301' OR '1'='1</c>.
/// Concatenated into SQL it would delete every account; as a parameter it is an account number nobody has.
/// </summary>
private static async Task Scenario2_SqlInTheValueToDeleteByAsync(SampleContext dbContext)
{
Step("2. SQL in the value to delete by deletes nothing");
var target = new ArxEntity("account");
target["account_number"] = "ACC-I301' OR '1'='1";
int deleted = await dbContext.Service.DeleteAsync(target, new ArxDeleteOptions { Match = ArxMatch.Key("account_number") });
Show($"{deleted} rows deleted");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*)
* FROM account
* WHERE account_number IN ('ACC-I301', 'ACC-I302', 'ACC-I303');
* -- Expect: 3: the text was compared as a value, so it matched no account.
* ===========================================================
*/
}
/// <summary>3. The column to delete by comes from the request: the SQL in it is refused; only allow columns from a fixed list.</summary>
private static async Task Scenario3_TheMatchColumnFromTheRequestAsync(SampleContext dbContext)
{
Step("3. The column to delete by, from the request, carries SQL");
var by = "name = name OR account_number";
var target = new ArxEntity("account");
target[by] = "x";
try
{
await dbContext.Service.DeleteAsync(target, new ArxDeleteOptions { Match = ArxMatch.Columns(by) });
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
SecurityLog(error);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*)
* FROM account
* WHERE name = 'Injection: keep me';
* -- Expect: 3.
* ===========================================================
*/
}
/// <summary>4. DeleteMultiple of the three ids with a table name carrying SQL: refused before the database, all three kept.</summary>
private static async Task Scenario4_DeleteMultipleWithABadTableAsync(SampleContext dbContext, IReadOnlyList<ArxRecordId> ids)
{
Step("4. DeleteMultiple with a table name carrying SQL");
try
{
await dbContext.Service.DeleteMultipleAsync("account\"; TRUNCATE account; --", ids.ToList());
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
SecurityLog(error);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT count(*)
* FROM account
* WHERE name = 'Injection: keep me';
* -- Expect: 3: every account is still there.
* ===========================================================
*/
}
}
Expected output
What dotnet run printed when the sample was verified, on a freshly created sample schema. Ids differ when the tables already hold rows.
> 1. A table name from the request carries SQL
SECURITY: ARX1017 SuspectedInjection: Entity name 'account WHERE 1=1; --' looks like an SQL injection attempt (contains ';'). ARX refused it before sending anything to the database. Names must come from your code or a fixed list, never from user input.
> 2. SQL in the value to delete by deletes nothing
0 rows deleted
> 3. The column to delete by, from the request, carries SQL
SECURITY: ARX1017 SuspectedInjection: Attribute name 'name = name OR account_number' looks like an SQL injection attempt (contains '='). ARX refused it before sending anything to the database. Names must come from your code or a fixed list, never from user input.
> 4. DeleteMultiple with a table name carrying SQL
SECURITY: ARX1017 SuspectedInjection: Entity name 'account"; TRUNCATE account; --' looks like an SQL injection attempt (contains '"'). ARX refused it before sending anything to the database. Names must come from your code or a fixed list, never from user input.