Prevent OData injection by using parameterized filters and escaping quote characters#1369
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
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
AzureTableClientquery 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.
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
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>
cgillum
reviewed
Jun 19, 2026
cgillum
approved these changes
Jun 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
$filterstrings inside single quotes but were not escaped. SinceKeySanitation.EscapePartitionKeydoes 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.:EscapePartitionKeyonly escapes storage-key characters (^ / \ # ?and control chars), not'. A crafted instance ID likea' or PartitionKey ne 'therefore changes the query's meaning.Changes
DurableTask.AzureStorage(usesAzure.Data.Tables)AzureTableQueryFilterwith atomic, self-escaping predicate builders —PartitionKeyEquals,ColumnEquals,PartitionKeyGreaterOrEqual,PartitionKeyLessThan— built onTableClient.CreateQueryFilter(...), which parameterizes and escapes values.PartitionKeyEqualsalso appliesKeySanitation.EscapePartitionKey.AzureTableTrackingStore(GetHistoryEntitiesResponseInfoAsync,RewindHistoryAsync,PurgeInstanceHistoryAsync,UpdateInstanceStatusForCompletedOrchestrationAsync) andOrchestrationInstanceStatusQueryCondition(TaskHubNames,InstanceIdPrefix,InstanceId).DurableTask.ServiceBus(builds filters fromstring.Formattemplates)EscapeODataValue(value) => value?.Replace("'", "''")helper, applied to user-influenced values inAzureTableClientquery 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
executionIdexecutionIdis escaped even though it's framework-generated at runtime, because the publicIOrchestrationServiceClient.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.TestWorkerFailingDuringCompleteWorkItemCallLargeInputOutputcontinues to exercise theUpdateInstanceStatuslarge-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.