Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3012.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3012
affected:
- src/CodeIndex/Mcp/McpServer.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP response-frame detection now enforces the JSON depth cap (#3012)** — response-frame probing rejects over-depth JSON before classification, matching the guarded MCP request and cancellation parsing paths.

## 日本語

- **MCP response frame 判定で JSON 深さ上限を適用しました (#3012)** — response frame の事前判定でも過深 JSON を分類前に拒否し、MCP request / cancellation の保護済み parse 経路と揃えました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3014.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3014
affected:
- src/CodeIndex/Mcp/HttpMcpTransport.cs
- tests/CodeIndex.Tests/HttpMcpTransportTests.cs
---

## English

- **HTTP MCP JSON-RPC id probing is covered by the JSON depth cap (#3014)** — over-depth HTTP request bodies no longer expose request-id extraction to unbounded JSON parsing work.

## 日本語

- **HTTP MCP の JSON-RPC id 抽出に JSON 深さ上限を適用しました (#3014)** — 過深な HTTP request body によって request id 抽出が無制限の JSON parse 作業を行わないようにしました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3016.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3016
affected:
- src/CodeIndex/Cli/JsonEnvelopeWrapper.cs
- tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs
---

## English

- **JSON envelope raw-item parsing now caps JSON depth (#3016)** — `--json-envelope` keeps over-depth raw output lines as strings instead of parsing them without an explicit nesting limit.

## 日本語

- **JSON envelope の raw item parse に JSON 深さ上限を適用しました (#3016)** — `--json-envelope` は過深な raw output 行を、明示的な入れ子上限なしで parse せず従来どおり文字列として保持します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3021.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3021
affected:
- src/CodeIndex/Lsp/LspServer.cs
- tests/CodeIndex.Tests/LspServerTests.cs
---

## English

- **LSP JSON message parsing now enforces a depth cap (#3021)** — deeply nested LSP payloads are rejected as parse errors instead of being parsed without an explicit nesting limit.

## 日本語

- **LSP JSON message parse に深さ上限を適用しました (#3021)** — 過深な LSP payload は、明示的な入れ子上限なしで parse されず parse error として拒否されます。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3022.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3022
affected:
- src/CodeIndex/Cli/QueryCommandRunner.cs
- tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
---

## English

- **Batch query JSON input now enforces a depth cap (#3022)** — `cdidx batch` rejects over-depth JSON lines during parsing before command argument processing.

## 日本語

- **batch query の JSON 入力に深さ上限を適用しました (#3022)** — `cdidx batch` は過深な JSON 行を command argument 処理前の parse 段階で拒否します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3036.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3036
affected:
- src/CodeIndex/Cli/ActiveWorkspace.cs
- tests/CodeIndex.Tests/WorkspaceCommandRunnerTests.cs
---

## English

- **Active workspace state parsing now enforces a JSON depth cap (#3036)** — deeply nested `active.json` files are ignored with the existing warning path instead of overriding workspace DB resolution.

## 日本語

- **active workspace state の parse に JSON 深さ上限を適用しました (#3036)** — 過深な `active.json` は既存の warning 経路で無視され、workspace DB 解決を上書きしません。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3043.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3043
affected:
- src/CodeIndex/Mcp/McpIndexRunLock.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP index-run lock metadata parsing now enforces a JSON depth cap (#3043)** — deeply nested lock holder metadata is treated as unavailable diagnostics instead of being parsed without an explicit nesting limit.

## 日本語

- **MCP index-run lock metadata の parse に JSON 深さ上限を適用しました (#3043)** — 過深な lock holder metadata は、明示的な入れ子上限なしで parse せず diagnostic 不明として扱います。
7 changes: 6 additions & 1 deletion src/CodeIndex/Cli/ActiveWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ internal static class ActiveWorkspace
{
internal const string EnvironmentVariable = "CDIDX_ACTIVE_WORKSPACE";
private const int MaxStateBytes = 64 * 1024;
internal const int MaxStateJsonDepth = 16;
private static readonly JsonDocumentOptions StateJsonDocumentOptions = new()
{
MaxDepth = MaxStateJsonDepth,
};

internal static string StatePath
{
Expand Down Expand Up @@ -41,7 +46,7 @@ internal static string StatePath
return null;
}

using var document = JsonDocument.Parse(text);
using var document = JsonDocument.Parse(text, StateJsonDocumentOptions);
var root = document.RootElement;
var name = ReadString(root, "name") ?? "default";
var workspaceRoot = ReadString(root, "root") ?? Environment.CurrentDirectory;
Expand Down
3 changes: 2 additions & 1 deletion src/CodeIndex/Cli/JsonEnvelopeWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal static class JsonEnvelopeWrapper
{
internal const string EnvelopeFlag = "--json-envelope";
internal const int MaxCapturedOutputChars = 10 * 1024 * 1024;
internal const int MaxRawJsonItemDepth = 32;

private static readonly HashSet<string> WrappableCommands = new(StringComparer.Ordinal)
{
Expand Down Expand Up @@ -210,7 +211,7 @@ private static JsonArray ParseRawJsonItems(string raw)
JsonNode? node;
try
{
node = JsonNode.Parse(line);
node = JsonNode.Parse(line, documentOptions: new JsonDocumentOptions { MaxDepth = MaxRawJsonItemDepth });
}
catch (JsonException)
{
Expand Down
8 changes: 7 additions & 1 deletion src/CodeIndex/Cli/QueryCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static class QueryCommandRunner
internal const int DefaultImpactLimit = 50;
internal const int BatchMaxLineChars = 1024 * 1024;
internal const int BatchMaxArgumentCount = 256;
internal const int BatchMaxJsonDepth = 32;
internal const string DefaultLimitEnvironmentVariable = "CDIDX_DEFAULT_LIMIT";
internal const string DefaultSnippetLinesEnvironmentVariable = "CDIDX_DEFAULT_SNIPPET_LINES";
internal const string DefaultMaxLineWidthEnvironmentVariable = "CDIDX_DEFAULT_MAX_LINE_WIDTH";
Expand All @@ -33,6 +34,11 @@ public static class QueryCommandRunner
private const string LanguageCapabilitySymbols = "symbols";
internal static readonly TimeSpan DefaultStaleAfter = TimeSpan.FromHours(24);
internal static TimeProvider TimeProvider { get; set; } = TimeProvider.System;
private static readonly JsonDocumentOptions BatchJsonDocumentOptions = new()
{
MaxDepth = BatchMaxJsonDepth,
};

[ThreadStatic]
private static DbReader? s_batchReader;

Expand Down Expand Up @@ -371,7 +377,7 @@ private static bool TryParseBatchLine(string line, int lineNumber, out string co

try
{
using var document = JsonDocument.Parse(line);
using var document = JsonDocument.Parse(line, BatchJsonDocumentOptions);
if (document.RootElement.ValueKind != JsonValueKind.Array || document.RootElement.GetArrayLength() == 0)
{
Console.Error.WriteLine($"Error: batch line {lineNumber} must be a non-empty JSON string array.");
Expand Down
10 changes: 8 additions & 2 deletions src/CodeIndex/Lsp/LspServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ internal sealed class LspServer : IDisposable
internal const int MaxLspFrameBytes = 8 * 1024 * 1024;
internal const int MaxLspHeaderLineBytes = 8 * 1024;
internal const int MaxPositionDocumentBytes = 4 * 1024 * 1024;
internal const int MaxJsonDepth = 32;
private static readonly JsonDocumentOptions LspJsonDocumentOptions = new()
{
MaxDepth = MaxJsonDepth,
};

private readonly DbReader _reader;
private readonly string _version;
private readonly JsonSerializerOptions _jsonOptions;
Expand Down Expand Up @@ -54,7 +60,7 @@ public int Run(Stream input, Stream output)
JsonDocument document;
try
{
document = JsonDocument.Parse(payload);
document = JsonDocument.Parse(payload, LspJsonDocumentOptions);
}
catch (JsonException)
{
Expand All @@ -74,7 +80,7 @@ public int Run(Stream input, Stream output)

var method = root.TryGetProperty("method", out var methodElement) ? methodElement.GetString() : null;
hasId = root.TryGetProperty("id", out var idElement);
id = hasId ? JsonNode.Parse(idElement.GetRawText()) : null;
id = hasId ? JsonNode.Parse(idElement.GetRawText(), documentOptions: LspJsonDocumentOptions) : null;

if (method == null)
return hasId ? Error(id, -32600, "Invalid Request") : null;
Expand Down
7 changes: 6 additions & 1 deletion src/CodeIndex/Mcp/McpIndexRunLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ internal sealed class McpIndexRunLock : IDisposable
{
internal const string LockFileName = "index.lock";
private const int MaxInfoBytes = 4 * 1024;
internal const int MaxInfoJsonDepth = 16;
private static readonly TimeSpan StaleInfoGracePeriod = TimeSpan.FromSeconds(2);
private static readonly JsonDocumentOptions InfoJsonDocumentOptions = new()
{
MaxDepth = MaxInfoJsonDepth,
};

private readonly FileStream _stream;
private readonly string _infoPath;
Expand Down Expand Up @@ -97,7 +102,7 @@ private static string BuildBusyMessage(string infoPath)
if (string.IsNullOrWhiteSpace(text))
return null;

using var document = JsonDocument.Parse(text);
using var document = JsonDocument.Parse(text, InfoJsonDocumentOptions);
var root = document.RootElement;
if (!root.TryGetProperty("pid", out var pidElement) || !pidElement.TryGetInt32(out var pid))
return null;
Expand Down
2 changes: 1 addition & 1 deletion src/CodeIndex/Mcp/McpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ private static bool IsServerResponseFrame(string frame)
{
try
{
var node = JsonNode.Parse(frame);
var node = JsonNode.Parse(frame, documentOptions: new JsonDocumentOptions { MaxDepth = MaxJsonDepth });
return node is JsonObject obj
&& obj.ContainsKey("id")
&& obj["method"] is null
Expand Down
22 changes: 22 additions & 0 deletions tests/CodeIndex.Tests/HttpMcpTransportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ public async Task HttpTransport_RequestLogger_RecordsMethodStatusDurationAndAuth
Assert.Equal((int)HttpStatusCode.OK, okPost.StatusCode);
}

[Fact]
public async Task HttpTransport_RequestLogger_TooDeepJsonRpcIdReturnsNull_Issue3014()
{
var records = new ConcurrentQueue<HttpMcpTransport.HttpRequestLogRecord>();
await using var harness = await McpHttpHarness.StartAsync(_dbPath, requestLogger: records.Enqueue);

using var response = await harness.PostJsonAsync(BuildNestedJsonRpcRequest(McpServer.MaxJsonDepth + 1));

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var snapshot = await WaitForRequestLogRecordsAsync(records, 1);
var record = Assert.Single(snapshot, record => record.Method == "POST");
Assert.Null(record.RequestId);
}

[Fact]
public async Task HttpTransport_TwoSequentialRequests_ShareWarmServer()
{
Expand Down Expand Up @@ -819,6 +833,14 @@ private static string BuildNestedJsonRpcResponse(int nestedObjectCount)
return builder.ToString();
}

private static string BuildNestedJsonRpcRequest(int nestedObjectCount)
{
var builder = new StringBuilder("""{"jsonrpc":"2.0","id":1,"method":"ping","params":""");
AppendNestedObject(builder, nestedObjectCount);
builder.Append('}');
return builder.ToString();
}

private static void AppendNestedObject(StringBuilder builder, int nestedObjectCount)
{
for (var i = 0; i < nestedObjectCount; i++)
Expand Down
38 changes: 38 additions & 0 deletions tests/CodeIndex.Tests/JsonEnvelopeWrapperTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using System.Text.Json;
using CodeIndex.Cli;

Expand Down Expand Up @@ -268,6 +269,29 @@ public void RunWrapped_CapturedOutputExceedsLimit_ReturnsJsonErrorEnvelope_Issue
Assert.Equal(0, document.RootElement.GetProperty("results").GetArrayLength());
}

[Fact]
public void RunWrapped_TooDeepRawJsonItem_KeepsLineAsString_Issue3016()
{
var rawLine = BuildNestedRawJson(JsonEnvelopeWrapper.MaxRawJsonItemDepth + 1);
var (exitCode, stdout, stderr) = CaptureConsole(() => JsonEnvelopeWrapper.RunWrapped(
"search",
["Needle", "--json-envelope"],
"1.0.0",
_jsonOptions,
_ =>
{
Console.WriteLine(rawLine);
return CommandExitCodes.Success;
}));

Assert.Equal(CommandExitCodes.Success, exitCode);
Assert.Equal(string.Empty, stderr);
using var document = JsonDocument.Parse(stdout);
var result = Assert.Single(document.RootElement.GetProperty("results").EnumerateArray());
Assert.Equal(JsonValueKind.String, result.ValueKind);
Assert.Equal(rawLine, result.GetString());
}

[Fact]
public void Symbols_WithEnvelope_NormalizesQueryFromExtraNames()
{
Expand Down Expand Up @@ -298,4 +322,18 @@ public void Symbols_WithEnvelope_NormalizesQueryFromExtraNames()

private static (int ExitCode, string Stdout, string Stderr) CaptureConsole(Func<int> action)
=> ConsoleCapture.Capture(action);

private static string BuildNestedRawJson(int nestedObjectCount)
{
var builder = new StringBuilder("""{"value":""");
for (var i = 0; i < nestedObjectCount; i++)
builder.Append("""{"next":""");

builder.Append('0');

for (var i = 0; i < nestedObjectCount; i++)
builder.Append('}');
builder.Append('}');
return builder.ToString();
}
}
36 changes: 36 additions & 0 deletions tests/CodeIndex.Tests/LspServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ public void HandleMessage_Initialize_AdvertisesCoreCapabilities()
}
}

[Fact]
public void HandleMessage_TooDeepJson_ReturnsParseError_Issue3021()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_depth");
try
{
var dbPath = TestProjectHelper.CreateProjectDb(projectRoot);
using var db = new DbContext(dbPath);
using var server = new LspServer(new DbReader(db), "1.2.3", ProgramRunner.CreateDefaultJsonOptions(), projectRoot);

var response = server.HandleMessage(BuildNestedLspRequest(LspServer.MaxJsonDepth + 1));

Assert.NotNull(response);
Assert.Equal(-32700, response!["error"]!["code"]!.GetValue<int>());
Assert.Null(response["id"]);
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void Run_MalformedJsonFrame_WritesParseErrorAndContinues()
{
Expand Down Expand Up @@ -667,6 +689,20 @@ private static int CharacterOf(string source, int line, string value)
return lines[line].IndexOf(value, StringComparison.Ordinal);
}

private static string BuildNestedLspRequest(int nestedObjectCount)
{
var builder = new StringBuilder("""{"jsonrpc":"2.0","id":1,"method":"initialize","params":""");
for (var i = 0; i < nestedObjectCount; i++)
builder.Append("""{"next":""");

builder.Append('0');

for (var i = 0; i < nestedObjectCount; i++)
builder.Append('}');
builder.Append('}');
return builder.ToString();
}

private static void MarkGraphReady(string dbPath)
{
using var db = new DbContext(dbPath);
Expand Down
Loading
Loading