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/3127.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 3127
affected:
- src/CodeIndex/Lsp/LspServer.cs
- tests/CodeIndex.Tests/LspServerTests.cs
---

## English

- **LSP unknown method diagnostics truncate method names (#3127)** — unknown JSON-RPC method names now use bounded diagnostic text before being echoed in `Method not found` responses.

## 日本語

- **LSP の未知 method 診断で method 名を切り詰めるようになりました (#3127)** — 未知 JSON-RPC method 名は `Method not found` response に埋め込む前に bounded diagnostic text へ変換されます。
20 changes: 19 additions & 1 deletion src/CodeIndex/Lsp/LspServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal sealed class LspServer : IDisposable
internal const int MaxPositionDocumentBytes = 4 * 1024 * 1024;
internal const int MaxTextDocumentUriChars = McpBoundedText.MaxResourceUriChars;
internal const int MaxJsonDepth = 32;
internal const int MaxUnknownMethodDiagnosticChars = 240;
private const int JsonRpcInvalidParamsCode = -32602;
private const int JsonRpcInternalErrorCode = -32603;
private const string JsonRpcInvalidParamsMessage = "Invalid params";
Expand Down Expand Up @@ -101,7 +102,7 @@ public int Run(Stream input, Stream output)
"textDocument/documentSymbol" => Result(id, DocumentSymbol(root)),
"textDocument/definition" => Result(id, Definition(root)),
"textDocument/references" => Result(id, References(root)),
_ => hasId ? Error(id, -32601, $"Method not found: {method}") : null,
_ => hasId ? Error(id, -32601, $"Method not found: {SanitizeUnknownMethod(method)}") : null,
};
}
catch (Exception ex) when (ex is ArgumentException or JsonException)
Expand All @@ -115,6 +116,23 @@ public int Run(Stream input, Stream output)
}
}

private static string SanitizeUnknownMethod(string method)
{
var wasTruncated = method.Length > MaxUnknownMethodDiagnosticChars;
var boundedMethod = wasTruncated ? method[..MaxUnknownMethodDiagnosticChars] : method;
var sanitized = boundedMethod
.Replace('\r', ' ')
.Replace('\n', ' ')
.Replace('\t', ' ')
.Trim();
return AppendEllipsisIfNeeded(sanitized, wasTruncated);
}

private static string AppendEllipsisIfNeeded(string value, bool wasTruncated)
=> wasTruncated && !value.EndsWith("...", StringComparison.Ordinal)
? value + "..."
: value;

private JsonObject HandleShutdown(JsonNode? id)
{
_shutdownRequested = true;
Expand Down
62 changes: 62 additions & 0 deletions tests/CodeIndex.Tests/LspServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,68 @@ public void HandleMessage_TooDeepJson_ReturnsParseError_Issue3021()
}
}

[Fact]
public void HandleMessage_UnknownMethod_TruncatesMethodName_Issue3127()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_unknown_method");
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 method = new string('m', LspServer.MaxLspFrameBytes - 4096) + "UNBOUNDED_SENTINEL";
var request = JsonSerializer.Serialize(new
{
jsonrpc = "2.0",
id = 1,
method,
});

var response = server.HandleMessage(request);

Assert.NotNull(response);
var error = response!["error"]!;
Assert.Equal(-32601, error["code"]!.GetValue<int>());
var message = error["message"]!.GetValue<string>();
Assert.StartsWith("Method not found: ", message, StringComparison.Ordinal);
Assert.EndsWith("...", message, StringComparison.Ordinal);
Assert.DoesNotContain("UNBOUNDED_SENTINEL", message, StringComparison.Ordinal);
Assert.True(message.Length <= "Method not found: ".Length + LspServer.MaxUnknownMethodDiagnosticChars + "...".Length);
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void HandleMessage_UnknownMethod_PreservesSlashDelimitedMethodName_Issue3127()
{
var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_unknown_method_slash");
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 request = JsonSerializer.Serialize(new
{
jsonrpc = "2.0",
id = 1,
method = "textDocument/hover",
});

var response = server.HandleMessage(request);

Assert.NotNull(response);
Assert.Equal(-32601, response!["error"]!["code"]!.GetValue<int>());
Assert.Equal("Method not found: textDocument/hover", response["error"]!["message"]!.GetValue<string>());
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void HandleMessage_InvalidParams_ReturnsStableErrorMessage_Issue3200()
{
Expand Down
Loading