Skip to content

Prevent OData injection by using parameterized filters and escaping quote characters#1369

Merged
AnatoliB merged 5 commits into
mainfrom
anatolib/odata-filter-injection-fix
Jun 19, 2026
Merged

Prevent OData injection by using parameterized filters and escaping quote characters#1369
AnatoliB merged 5 commits into
mainfrom
anatolib/odata-filter-injection-fix

Conversation

@AnatoliB

@AnatoliB AnatoliB commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Prevent OData injection in Azure Table query filters

Summary

Hardens Azure Table Storage OData filter construction against injection. User-influenced values — orchestration instance IDs, instance-ID prefixes, task hub names, and execution IDs — were interpolated into OData $filter strings inside single quotes but were not escaped. Since KeySanitation.EscapePartitionKey does not escape single quotes, a value containing ' could alter or break the generated query.

The problem

OData string literals are single-quoted, so a literal ' must be doubled ('''). Several filters were built via raw interpolation, e.g.:

string filter = $"PartitionKey eq '{KeySanitation.EscapePartitionKey(instanceId)}'";

EscapePartitionKey only escapes storage-key characters (^ / \ # ? and control chars), not '. A crafted instance ID like a' or PartitionKey ne ' therefore changes the query's meaning.

Changes

DurableTask.AzureStorage (uses Azure.Data.Tables)

  • New internal helper AzureTableQueryFilter with atomic, self-escaping predicate builders — PartitionKeyEquals, ColumnEquals, PartitionKeyGreaterOrEqual, PartitionKeyLessThan — built on TableClient.CreateQueryFilter(...), which parameterizes and escapes values. PartitionKeyEquals also applies KeySanitation.EscapePartitionKey.
  • Refactored all filter-building sites to use it: AzureTableTrackingStore (GetHistoryEntitiesResponseInfoAsync, RewindHistoryAsync, PurgeInstanceHistoryAsync, UpdateInstanceStatusForCompletedOrchestrationAsync) and OrchestrationInstanceStatusQueryCondition (TaskHubNames, InstanceIdPrefix, InstanceId).

DurableTask.ServiceBus (builds filters from string.Format templates)

  • Added a small EscapeODataValue(value) => value?.Replace("'", "''") helper, applied to user-influenced values in AzureTableClient query generation (instance ID, execution ID, name, version, status, partition/row-key ranges).

The two approaches reflect the different table SDKs: AzureStorage can use the modern parameterized CreateQueryFilter; ServiceBus builds filters from format-string templates, so values are escaped before formatting.

A note on executionId

executionId is escaped even though it's framework-generated at runtime, because the public IOrchestrationServiceClient.GetOrchestrationHistoryAsync(instanceId, executionId) API forwards a caller-supplied execution ID into the history query. Escaping all filter values uniformly also avoids fragile per-value "is this user-controlled?" judgments.

Testing

  • AzureTableQueryFilterTests (new) — data-driven unit tests per helper: normal values, single-quote escaping, empty value, partition-key sanitization.
  • OrchestrationInstanceStatusQueryConditionTest (new cases) — single-quote escaping for task hub name, instance-ID prefix, instance ID.
  • AzureTableClientTest (new cases) — single-quote escaping in ServiceBus queries.
  • PurgeInstanceHistory_InstanceIdWithSingleQuote_Succeeds (new) — end-to-end purge with a single-quote instance ID; verified to fail without the fix.
  • Existing TestWorkerFailingDuringCompleteWorkItemCallLargeInputOutput continues to exercise the UpdateInstanceStatus large-payload paths.

Filter output is byte-identical to prior behavior for inputs without special characters (confirmed by the unchanged composition tests).

Compatibility

No behavior change for normal inputs — only quote handling differs. Compile-time constants (sentinel row key, event-type names) flow through the same helpers for consistency; escaping them is a harmless no-op.

Copilot AI review requested due to automatic review settings June 17, 2026 22:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens Azure Table Storage OData filter construction to reduce the risk of OData injection (primarily by ensuring single quotes in user-influenced values are safely handled) and adds regression tests to validate escaping behavior.

Changes:

  • Added single-quote escaping for OData filter values in the ServiceBus AzureTableClient query generation.
  • Updated AzureStorage query filter construction to use TableClient.CreateQueryFilter(...) for parameterized, correctly-escaped filters.
  • Added unit tests covering single-quote escaping in instance ID, execution ID, name, task hub name, and prefix filters.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/DurableTask.ServiceBus.Tests/AzureTableClientTest.cs Adds tests validating single-quote escaping in generated ServiceBus table queries.
test/DurableTask.AzureStorage.Tests/OrchestrationInstanceStatusQueryConditionTest.cs Adds tests validating single-quote escaping in AzureStorage status query filters.
src/DurableTask.ServiceBus/Tracking/AzureTableClient.cs Escapes single quotes in values inserted into OData filter templates.
src/DurableTask.AzureStorage/Tracking/OrchestrationInstanceStatusQueryCondition.cs Switches to TableClient.CreateQueryFilter for safer, parameterized OData filter generation.
src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs Updates some history query filters to use TableClient.CreateQueryFilter.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs Outdated
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 19, 2026 02:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread test/DurableTask.AzureStorage.Tests/AzureTableQueryFilterTests.cs
The file was tracked under Test/ (uppercase) while the test project and solution live under test/ (lowercase). On case-sensitive filesystems (e.g. CI) the file would not be part of the project, so the new tests would not be compiled or run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@AnatoliB AnatoliB marked this pull request as ready for review June 19, 2026 16:27
Copilot AI review requested due to automatic review settings June 19, 2026 16:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comment thread src/DurableTask.AzureStorage/Tracking/AzureTableQueryFilter.cs Outdated
Comment thread src/DurableTask.AzureStorage/Tracking/AzureTableQueryFilter.cs Outdated
@AnatoliB AnatoliB merged commit 507a7dd into main Jun 19, 2026
47 checks passed
@AnatoliB AnatoliB deleted the anatolib/odata-filter-injection-fix branch June 19, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants