From 51af3f9f0e6e2f171f4cbf8ad8fec4253d1e2d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Mon, 5 Oct 2026 09:10:41 +0200 Subject: [PATCH] Reject oversized filter array indexes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ .../CommandTests/FilterCommandTests.cs | 17 ++++++++++++++ .../Parser/FilterPathExpressionTests.cs | 22 +++++++++++++++++++ .../ExpressionParser.cs | 7 +++++- README.md | 2 +- docs/filter-v1-spec.md | 1 + 6 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 760da1d1..18ddbee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Reject oversized `filter` array indexes instead of silently selecting element zero. + ## 1.1.271-preview — 2026-10-02 ### New features diff --git a/CosmosDBShell.Tests/CommandTests/FilterCommandTests.cs b/CosmosDBShell.Tests/CommandTests/FilterCommandTests.cs index 86cfd865..5f4d43dd 100644 --- a/CosmosDBShell.Tests/CommandTests/FilterCommandTests.cs +++ b/CosmosDBShell.Tests/CommandTests/FilterCommandTests.cs @@ -11,6 +11,23 @@ namespace CosmosShell.Tests.CommandTests; public class FilterCommandTests { + [Theory] + [InlineData(".[2147483648]")] + [InlineData(".[2147483648]?")] + [InlineData(".[999999999999999999999999999999]")] + public async Task ExecuteAsync_RejectsOversizedIndex_WithoutChangingInput(string expression) + { + using var shell = ShellInterpreter.CreateInstance(); + var input = new ShellJson(JsonSerializer.SerializeToElement(new[] { 10, 20 })); + var state = new CommandState { Result = input }; + var command = new FilterCommand { ExpressionText = expression }; + + await Assert.ThrowsAsync(() => + command.ExecuteAsync(shell, state, string.Empty, CancellationToken.None)); + + Assert.Same(input, state.Result); + } + [Fact] public async Task ExecuteAsync_AppliesPathExpression_AndPreservesStructuredResult() { diff --git a/CosmosDBShell.Tests/Parser/FilterPathExpressionTests.cs b/CosmosDBShell.Tests/Parser/FilterPathExpressionTests.cs index d072424a..eb3e9861 100644 --- a/CosmosDBShell.Tests/Parser/FilterPathExpressionTests.cs +++ b/CosmosDBShell.Tests/Parser/FilterPathExpressionTests.cs @@ -70,6 +70,28 @@ public async Task Index_OutOfRange_ReturnsNull() Assert.Equal(JsonValueKind.Null, Assert.IsType(result).Value.ValueKind); } + [Theory] + [InlineData(".[2147483648]")] + [InlineData(".items[2147483648]?")] + [InlineData(".[999999999999999999999999999999]")] + public void Index_Overflow_ReportsErrorAtIndex(string input) + { + var lexer = new Lexer(input); + var expression = new ExpressionParser(lexer).ParseFilterExpression(); + + Assert.IsType(expression); + Assert.True(lexer.Errors.HasErrors); + var error = Assert.Single(lexer.Errors); + Assert.Equal(input.IndexOf('[') + 1, error.Start); + } + + [Fact] + public async Task Index_MaximumInt32_ReturnsNull() + { + var result = await EvalAsync(".[2147483647]", new[] { 10, 20, 30 }); + Assert.Equal(JsonValueKind.Null, Assert.IsType(result).Value.ValueKind); + } + [Fact] public async Task Index_OnNonArrayWithoutOptional_Throws() => await Assert.ThrowsAsync(() => EvalAsync(".[0]", new { a = 1 })); diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ExpressionParser.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ExpressionParser.cs index 445100cb..a69502ca 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ExpressionParser.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ExpressionParser.cs @@ -1722,7 +1722,12 @@ private Expression ParseFilterPathExpression(Token firstToken) } var indexToken = this.Consume(TokenType.Number, MessageService.GetString("expression_error_expected_array_index")); - int index = int.TryParse(indexToken.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedIndex) ? parsedIndex : 0; + if (!int.TryParse(indexToken.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index)) + { + this.ReportError(MessageService.GetArgsString("expression_error_invalid_number", "value", indexToken.Value), indexToken); + return new ErrorExpression(indexToken.Start, indexToken.Length); + } + var indexedCloseBracket = this.Consume(TokenType.CloseBracket, MessageService.GetString("expression_error_expected_close_bracket")); var indexQuestionToken = this.TryConsumeQuestion(); end = indexedCloseBracket.Start + indexedCloseBracket.Length; diff --git a/README.md b/README.md index d25bce44..9f5e40ad 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ A terminal-native shell for Azure Cosmos DB — navigate databases like a filesy - Tail the change feed of a container with `watch` (alias `tail`) - Database and container management commands prefer Azure Resource Manager when connected with Entra ID, with data-plane fallback for key, emulator, and static-token connections - Pipelines and scripting with variables, loops, functions -- Transform piped JSON output with `filter` using jq-inspired expressions (field access, indexing, `map`, `length`, pipelines) +- Transform piped JSON output with `filter` using jq-inspired expressions (field access, indexing, `map`, `length`, pipelines). Indexes larger than `2147483647` are rejected; valid indexes beyond the array length return `null`. - Edit local files in your external editor with `edit`, and customize REPL colors with `theme` (`list`, `show`, `use`, `load`, `validate`, `save`, `edit`; built-in default/light/dark/monochrome) - Multi-line input at the prompt — automatic continuation for unclosed blocks/strings, plus explicit `\` line continuation ([docs](docs/navigation.md#multi-line-input)) - MCP server for AI/tool integration diff --git a/docs/filter-v1-spec.md b/docs/filter-v1-spec.md index 98a53c81..6fde0e35 100644 --- a/docs/filter-v1-spec.md +++ b/docs/filter-v1-spec.md @@ -28,6 +28,7 @@ filter - `expression` is required. - The expression is usually quoted at the shell level, for example: `filter '.items[0]'`. +- Array indexes must be integer literals between `0` and `2147483647`. Larger indexes are rejected, including optional paths (`?`); valid indexes beyond the array length return `null`. - Input comes from the current pipeline value. - Output is written back into the shell's structured command result.