diff --git a/changelog.d/unreleased/3200.security.md b/changelog.d/unreleased/3200.security.md new file mode 100644 index 0000000000..88d5a47d36 --- /dev/null +++ b/changelog.d/unreleased/3200.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 3200 +affected: + - src/CodeIndex/Lsp/LspServer.cs + - tests/CodeIndex.Tests/LspServerTests.cs +--- + +## English + +- **LSP errors no longer expose raw exception messages (#3200)** — LSP invalid-params and internal-failure responses now return stable JSON-RPC messages instead of forwarding implementation exception text to clients. + +## 日本語 + +- **LSP エラーが raw 例外メッセージを公開しなくなりました (#3200)** — LSP の invalid params / internal failure 応答は、実装内部の例外文をクライアントへ転送せず、安定した JSON-RPC メッセージを返すようになりました。 diff --git a/src/CodeIndex/Lsp/LspServer.cs b/src/CodeIndex/Lsp/LspServer.cs index dd86becd23..401d62623e 100644 --- a/src/CodeIndex/Lsp/LspServer.cs +++ b/src/CodeIndex/Lsp/LspServer.cs @@ -16,6 +16,10 @@ internal sealed class LspServer : IDisposable internal const int MaxLspHeaderLineBytes = 8 * 1024; internal const int MaxPositionDocumentBytes = 4 * 1024 * 1024; internal const int MaxJsonDepth = 32; + private const int JsonRpcInvalidParamsCode = -32602; + private const int JsonRpcInternalErrorCode = -32603; + private const string JsonRpcInvalidParamsMessage = "Invalid params"; + private const string JsonRpcInternalErrorMessage = "Internal error"; private static readonly JsonDocumentOptions LspJsonDocumentOptions = new() { MaxDepth = MaxJsonDepth, @@ -98,9 +102,13 @@ public int Run(Stream input, Stream output) _ => hasId ? Error(id, -32601, $"Method not found: {method}") : null, }; } - catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or JsonException or IOException) + catch (Exception ex) when (ex is ArgumentException or JsonException) { - return hasId ? Error(id, -32602, ex.Message) : null; + return hasId ? Error(id, JsonRpcInvalidParamsCode, JsonRpcInvalidParamsMessage) : null; + } + catch (Exception ex) when (ex is InvalidOperationException or IOException) + { + return hasId ? Error(id, JsonRpcInternalErrorCode, JsonRpcInternalErrorMessage) : null; } } } diff --git a/tests/CodeIndex.Tests/LspServerTests.cs b/tests/CodeIndex.Tests/LspServerTests.cs index 013906373e..808fe62609 100644 --- a/tests/CodeIndex.Tests/LspServerTests.cs +++ b/tests/CodeIndex.Tests/LspServerTests.cs @@ -105,6 +105,75 @@ public void HandleMessage_TooDeepJson_ReturnsParseError_Issue3021() } } + [Fact] + public void HandleMessage_InvalidParams_ReturnsStableErrorMessage_Issue3200() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_invalid_params"); + 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 = 20, + method = "textDocument/documentSymbol", + @params = new + { + textDocument = new { uri = string.Empty }, + }, + }); + + var response = server.HandleMessage(request); + + Assert.NotNull(response); + Assert.Equal(-32602, response!["error"]!["code"]!.GetValue()); + var message = response["error"]!["message"]!.GetValue(); + Assert.Equal("Invalid params", message); + Assert.DoesNotContain("textDocument.uri", message, StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void HandleMessage_InternalFailure_ReturnsStableErrorMessage_Issue3200() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_internal_error"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var db = new DbContext(dbPath); + using var server = new LspServer(new DbReader(db), "1.2.3", ProgramRunner.CreateDefaultJsonOptions(), projectRoot); + db.Dispose(); + var request = JsonSerializer.Serialize(new + { + jsonrpc = "2.0", + id = 21, + method = "workspace/symbol", + @params = new + { + query = "Needle", + }, + }); + + var response = server.HandleMessage(request); + + Assert.NotNull(response); + Assert.Equal(-32603, response!["error"]!["code"]!.GetValue()); + var message = response["error"]!["message"]!.GetValue(); + Assert.Equal("Internal error", message); + Assert.DoesNotContain(nameof(ObjectDisposedException), message, StringComparison.Ordinal); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_MalformedJsonFrame_WritesParseErrorAndContinues() {