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/3200.security.md
Original file line number Diff line number Diff line change
@@ -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 メッセージを返すようになりました。
12 changes: 10 additions & 2 deletions src/CodeIndex/Lsp/LspServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
}
}
Expand Down
69 changes: 69 additions & 0 deletions tests/CodeIndex.Tests/LspServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>());
var message = response["error"]!["message"]!.GetValue<string>();
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<int>());
var message = response["error"]!["message"]!.GetValue<string>();
Assert.Equal("Internal error", message);
Assert.DoesNotContain(nameof(ObjectDisposedException), message, StringComparison.Ordinal);
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

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