diff --git a/changelog.d/unreleased/3127.security.md b/changelog.d/unreleased/3127.security.md new file mode 100644 index 0000000000..e29338e2e8 --- /dev/null +++ b/changelog.d/unreleased/3127.security.md @@ -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 へ変換されます。 diff --git a/src/CodeIndex/Lsp/LspServer.cs b/src/CodeIndex/Lsp/LspServer.cs index ca69278320..d706360d08 100644 --- a/src/CodeIndex/Lsp/LspServer.cs +++ b/src/CodeIndex/Lsp/LspServer.cs @@ -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"; @@ -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) @@ -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; diff --git a/tests/CodeIndex.Tests/LspServerTests.cs b/tests/CodeIndex.Tests/LspServerTests.cs index 7b316afd2a..8706c17011 100644 --- a/tests/CodeIndex.Tests/LspServerTests.cs +++ b/tests/CodeIndex.Tests/LspServerTests.cs @@ -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()); + var message = error["message"]!.GetValue(); + 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()); + Assert.Equal("Method not found: textDocument/hover", response["error"]!["message"]!.GetValue()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void HandleMessage_InvalidParams_ReturnsStableErrorMessage_Issue3200() {