Amazon DynamoDB · RetrieveMultiple
Catch an injection attempt
A search endpoint whose sort and filter fields come from the request, attacked three ways: expression syntax in a filter name, an index name that isn't in the model, and a partition value written to read every customer; then the honest search.
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); SampleModel.cs (the DynamoDB layout and the schema catalog); SampleContext.Direct.cs (dbContext.Direct); DynamoDirectHelper.cs (the checks behind dbContext.Direct).
dbContext.Direct is the samples' own checking code on the plain driver, not part of ARX: leave it out of your application.
Before scenario 1 the sample runs this, so the service and the rows the scenarios use are there:
foreach (var (customerId, number) in new[] { ("C-D451", "SO-D451"), ("C-D452", "SO-D452") })
{
var order = new ArxEntity("sales_order");
order["customer_id"] = customerId;
order["order_number"] = number;
order["status"] = "OPEN";
order["ordered_on"] = "2026-11-01";
await service.CreateAsync(order);
}1 Expression syntax in a filter
A filter on "status OR attribute_exists(PK)": ARX1017.
Already in scope: service (ArxDynamoDbService)
var query = OrdersOf("C-D451");
query.Criteria.AddCondition("status OR attribute_exists(PK)", ArxConditionOperator.Equal, "OPEN");
try
{
await service.RetrieveMultipleAsync(query);
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.SuspectedInjection)
{
Console.WriteLine(exception.Message);
}Defined once in this sample.
private static ArxQueryExpression OrdersOf(string customerId)
{
var query = new ArxQueryExpression("sales_order");
query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, customerId);
return query;
}Check it in the database
A PartiQL statement: run it in NoSQL Workbench, or with aws dynamodb execute-statement --statement, after the code has run.
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D451' AND begins_with("SK", 'ORDER#')Expect: 1 item, SO-D451.
2 An unknown index
An index name from the request that the model doesn't have: refused before DynamoDB.
Already in scope: service (ArxDynamoDbService)
try
{
await service.RetrieveMultipleAsync(OrdersOf("C-D451"), new ArxDynamoReadOptions { IndexName = "AllCustomersIndex" });
}
catch (ArxException exception)
{
Console.WriteLine(exception.Message);
}Defined once in this sample.
private static ArxQueryExpression OrdersOf(string customerId)
{
var query = new ArxQueryExpression("sales_order");
query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, customerId);
return query;
}Check it in the database
Note: In the AWS CLI: aws dynamodb describe-table --table-name crm --endpoint-url http://localhost:18000 one global secondary index, GSI1.
3 A value that widens the read
The customer id "C-D451' OR '1'='1": one partition that doesn't exist, so no orders at all.
Already in scope: service (ArxDynamoDbService)
var found = await service.RetrieveMultipleAsync(OrdersOf("C-D451' OR '1'='1"));
Console.WriteLine($"{found.Entities.Count} orders");
var own = await service.RetrieveMultipleAsync(OrdersOf("C-D451"));
Console.WriteLine($"the honest search: {string.Join(", ", own.Entities.Select(x => x["order_number"]))}");Defined once in this sample.
private static ArxQueryExpression OrdersOf(string customerId)
{
var query = new ArxQueryExpression("sales_order");
query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, customerId);
return query;
}Check it in the database
A PartiQL statement: run it in NoSQL Workbench, or with aws dynamodb execute-statement --statement, after the code has run.
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D451'' OR ''1''=''1'Expect: no items.
Good to know
Names reach DynamoDB as placeholders, so expression syntax in one is ARX1017. An index name is checked against the model. A value is only a value: the partition it names is one key, and ARX sends no PartiQL.
See also
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.DynamoDb --version 1.0.0-rc.1 - Database: a Amazon DynamoDB, or DynamoDB Local for development (tested with DynamoDB Local 2.5.2) database with the sample schema.
- Configuration: set
ARX_SAMPLES_DYNAMODBto 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,SampleModel.cs,SampleContext.Direct.cs,DynamoDirectHelper.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 and first remakes the sample database from sample-schema.json.
using ArxSamples.DynamoDb;
using ArxSamples.DynamoDb.Operations.RetrieveMultiple.RealCases;
// The connection string comes from the environment, never from the code (see "Configure the connection").
var connectionString = Environment.GetEnvironmentVariable("ARX_SAMPLES_DYNAMODB")
?? throw new InvalidOperationException("Set the ARX_SAMPLES_DYNAMODB environment variable to your Amazon DynamoDB connection string.");
var dbContext = new SampleContext(connectionString);
// Every run starts from the sample data: this deletes and makes again what sample-schema.json lists
// (save the file in the project folder, see Get started).
await dbContext.Direct.ResetAsync();
await new RealCase3_CatchAnInjectionAttempt().RunAsync(dbContext);
The sample, as it is in the repository.
using ARX.SDK;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Query;
using ARX.SDK.Services;
using static ArxSamples.DynamoDb.SampleOutput;
namespace ArxSamples.DynamoDb.Operations.RetrieveMultiple.RealCases;
/// <summary>
/// A search endpoint whose sort and filter fields come from the request, attacked three ways: expression syntax in a
/// filter name, an index name that isn't in the model, and a partition value written to read every customer; then the
/// honest search.
/// </summary>
/// <remarks>
/// Names reach DynamoDB as placeholders, so expression syntax in one is ARX1017. An index name is checked against the
/// model. A value is only a value: the partition it names is one key, and ARX sends no PartiQL.
/// </remarks>
public sealed class RealCase3_CatchAnInjectionAttempt : ISample
{
public string Name => "retrieve-multiple-real-3-catch-an-injection-attempt";
public string Title => "Catch an injection attempt";
public async Task RunAsync(SampleContext dbContext)
{
foreach (var (customerId, number) in new[] { ("C-D451", "SO-D451"), ("C-D452", "SO-D452") })
{
var order = new ArxEntity("sales_order");
order["customer_id"] = customerId;
order["order_number"] = number;
order["status"] = "OPEN";
order["ordered_on"] = "2026-11-01";
await dbContext.Service.CreateAsync(order);
}
await Scenario1_ExpressionSyntaxInAFilterAsync(dbContext.Service);
await Scenario2_AnUnknownIndexAsync(dbContext.Service);
await Scenario3_AValueThatWidensTheReadAsync(dbContext.Service);
}
private static ArxQueryExpression OrdersOf(string customerId)
{
var query = new ArxQueryExpression("sales_order");
query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, customerId);
return query;
}
/// <summary>1. A filter on "status OR attribute_exists(PK)": ARX1017.</summary>
private static async Task Scenario1_ExpressionSyntaxInAFilterAsync(ArxDynamoDbService service)
{
Step("1. Expression syntax in a filter");
var query = OrdersOf("C-D451");
query.Criteria.AddCondition("status OR attribute_exists(PK)", ArxConditionOperator.Equal, "OPEN");
try
{
await service.RetrieveMultipleAsync(query);
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.SuspectedInjection)
{
ShowError(exception);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D451' AND begins_with("SK", 'ORDER#')
* -- Expect: 1 item, SO-D451.
* ===========================================================
*/
}
/// <summary>2. An index name from the request that the model doesn't have: refused before DynamoDB.</summary>
private static async Task Scenario2_AnUnknownIndexAsync(ArxDynamoDbService service)
{
Step("2. An unknown index");
try
{
await service.RetrieveMultipleAsync(OrdersOf("C-D451"), new ArxDynamoReadOptions { IndexName = "AllCustomersIndex" });
}
catch (ArxException exception)
{
ShowError(exception);
}
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* -- In the AWS CLI: aws dynamodb describe-table --table-name crm --endpoint-url http://localhost:18000
* -- Expect: one global secondary index, GSI1.
* ===========================================================
*/
}
/// <summary>3. The customer id "C-D451' OR '1'='1": one partition that doesn't exist, so no orders at all.</summary>
private static async Task Scenario3_AValueThatWidensTheReadAsync(ArxDynamoDbService service)
{
Step("3. A value that widens the read");
var found = await service.RetrieveMultipleAsync(OrdersOf("C-D451' OR '1'='1"));
Show($"{found.Entities.Count} orders");
var own = await service.RetrieveMultipleAsync(OrdersOf("C-D451"));
Show($"the honest search: {string.Join(", ", own.Entities.Select(x => x["order_number"]))}");
/* ===========================================================
* [DIRECT QUERY CHECK]
* -----------------------------------------------------------
* SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D451'' OR ''1''=''1'
* -- Expect: no items.
* ===========================================================
*/
}
}
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. Expression syntax in a filter
caught ArxValidationException: ARX1017 SuspectedInjection: Attribute name 'status OR attribute_exists(PK)' 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. An unknown index
caught ArxValidationException: ARX1001 InvalidArgument: 'sales_order' has no index 'AllCustomersIndex' in the model.
> 3. A value that widens the read
0 orders
the honest search: SO-D451