Root cause
Injection & unsafe execution introduced
feat: enhance get_diagnostics with analyzer support
How AI contributed: Direct introductionPublished May 29, 2026 in
marcelroozekrans/roslyn-codelens-mcp
get_diagnostics MCP tool loads and executes every DiagnosticAnalyzer assembly referenced by the target solution (AnalyzerRunner uses compilation.WithAnalyzers over all project.AnalyzerReferences) with includeAnalyzers defaulting to true and no allowlist, signature check, or user confirmation; a malicious .csproj referencing an attacker-controlled analyzer DLL yields arbitrary code execution in the MCP server process. Fixed in 1.17.0.
Root cause
feat: enhance get_diagnostics with analyzer support
Fix
feat(security): trust gate + analyzer allowlist for GHSA-552p-8f74-6x7q (#168)
AI-assisted fix: Claude Code · Marcel Roozekrans
Case facts
@@ -9,13 +9,79 @@ public static class GetDiagnosticsLogic { public static List<DiagnosticInfo> Execute(LoadedSolution loaded, SymbolResolver resolver, string? project, string? severity) {- var minSeverity = severity?.ToLowerInvariant() switch+ return CollectCompilerDiagnostics(loaded, resolver, project, severity);+ }++ public static async Task<List<DiagnosticInfo>> ExecuteAsync(+ LoadedSolution loaded,+ SymbolResolver resolver,+ string? project,+ string? severity,+ bool includeAnalyzers,+ CancellationToken ct = default)+ {+ var results = CollectCompilerDiagnostics(loaded, resolver, project, severity);++ if (includeAnalyzers)+ {+ var runner = new AnalyzerRunner();+ var minSeverity = ParseMinSeverity(severity);++ foreach (var (projectId, compilation) in loaded.Compilations)+ {+ var projectName = resolver.GetProjectName(projectId);++ if (project != null &&+ !projectName.Contains(project, StringComparison.OrdinalIgnoreCase))+ continue;++ var roslynProject = loaded.Solution.GetProject(projectId);+ if (roslynProject == null)+ continue;++ var analyzerDiagnostics = await runner.RunAnalyzersAsync(roslynProject, compilation, ct);++ foreach (var diagnostic in analyzerDiagnostics)+ {+ if (diagnostic.Severity < minSeverity)+ continue;++ var lineSpan = diagnostic.Location.GetLineSpan();+ var file = lineSpan.Path ?? "";+ var line = lineSpan.StartLinePosition.Line + 1;++ results.Add(new DiagnosticInfo(+ diagnostic.Id,+ diagnostic.Severity.ToString(),+ diagnostic.GetMessage(),+ file,+ line,+ projectName,+ $"analyzer:{diagnostic.Id}"));+ }+ }+ }++ return results;+ }++ private static DiagnosticSeverity ParseMinSeverity(string? severity)+ {+ return severity?.ToLowerInvariant() switch { "error" => DiagnosticSeverity.Error, "warning" => DiagnosticSeverity.Warning,- _ => DiagnosticSeverity.Warning // default: show warnings and errors+ _ => DiagnosticSeverity.Warning };+ } + private static List<DiagnosticInfo> CollectCompilerDiagnostics(+ LoadedSolution loaded,+ SymbolResolver resolver,+ string? project,+ string? severity)+ {+ var minSeverity = ParseMinSeverity(severity); var results = new List<DiagnosticInfo>(); foreach (var (projectId, compilation) in loaded.Compilations)@@ -53,14 +119,16 @@ public static List<DiagnosticInfo> Execute(LoadedSolution loaded, SymbolResolver public static class GetDiagnosticsTool { [McpServerTool(Name = "get_diagnostics"),- Description("List compiler errors and warnings across the solution")]- public static List<DiagnosticInfo> Execute(+ Description("List compiler errors and warnings across the solution, optionally including analyzer diagnostics")]+ public static async Task<List<DiagnosticInfo>> Execute(@@ -36,4 +36,30 @@ public void GetDiagnostics_WithProjectFilter_FiltersResults() Assert.True(filtered.Count <= all.Count); Assert.All(filtered, d => Assert.Contains("TestLib", d.Project)); }++ [Fact]+ public async Task GetDiagnostics_WithAnalyzers_IncludesAnalyzerDiagnostics()+ {+ var results = await GetDiagnosticsLogic.ExecuteAsync(+ _loaded, _resolver, null, null, includeAnalyzers: true);++ // Any analyzer diagnostics should have Source starting with "analyzer"+ var analyzerResults = results.Where(d => d.Source.StartsWith("analyzer")).ToList();+ // Compiler diagnostics should still be present with Source == "compiler"+ var compilerResults = results.Where(d => d.Source == "compiler").ToList();++ // All results should have a valid source+ Assert.All(results, d => Assert.True(+ d.Source == "compiler" || d.Source.StartsWith("analyzer:"),+ $"Unexpected source: {d.Source}"));+ }++ [Fact]+ public async Task GetDiagnostics_WithoutAnalyzers_OnlyCompilerDiagnostics()+ {+ var results = await GetDiagnosticsLogic.ExecuteAsync(+ _loaded, _resolver, null, null, includeAnalyzers: false);++ Assert.All(results, d => Assert.Equal("compiler", d.Source));+ } }@@ -0,0 +1,22 @@+using System.ComponentModel;+using ModelContextProtocol.Server;+using RoslynCodeLens.Security;++namespace RoslynCodeLens.Tools;++[McpServerToolType]+public static class TrustSolutionTool+{+ [McpServerTool(Name = "trust_solution"),+ Description("Mark a solution path (or directory root) as trusted for analyzer execution. " ++ "Required before get_diagnostics will load Roslyn analyzer DLLs from the solution. " ++ "Always confirm with the user before calling this tool — analyzer DLLs run as in-process code.")]+ public static TrustSolutionResult Execute(+ TrustStore trustStore,+ [Description("Absolute path to a .sln/.slnx file, or a directory when scope='addRoot'")] string path,+ [Description("'session' (in-memory only, default), 'persistent' (write to trust.json), or 'addRoot' (trust the directory and all solutions under it)")]+ string scope = "session")+ {+ return TrustSolutionLogic.Execute(trustStore, path, scope);+ }+}@@ -0,0 +1,29 @@+using RoslynCodeLens.Security;++namespace RoslynCodeLens.Tools;++public static class TrustSolutionLogic+{+ public static TrustSolutionResult Execute(TrustStore store, string path, string scope)+ {+ switch (scope)+ {+ case "session":+ store.AddSessionTrust(path);+ break;+ case "persistent":+ store.AddPersistentTrust(path);+ break;+ case "addRoot":+ store.AddTrustedRoot(path);+ break;+ default:+ throw new ArgumentException(+ $"Unknown scope '{scope}'. Use 'session', 'persistent', or 'addRoot'.",+ nameof(scope));+ }+ return new TrustSolutionResult(path, scope, "Solution trusted.");+ }+}++public sealed record TrustSolutionResult(string Path, string Scope, string Message);@@ -0,0 +1,19 @@+using System.ComponentModel;+using ModelContextProtocol.Server;+using RoslynCodeLens.Security;++namespace RoslynCodeLens.Tools;++[McpServerToolType]+public static class RevokeTrustTool+{+ [McpServerTool(Name = "revoke_trust"),+ Description("Remove a previously trusted solution path or trusted root. Removes both session and persistent entries.")]+ public static string Execute(+ TrustStore trustStore,+ [Description("Absolute path of the solution or trusted root to revoke")] string path)+ {+ trustStore.Revoke(path);+ return $"Trust revoked for: {path}";+ }+}AI-assisted change fa4028c39f93ff3f4100c1686674d0d92ef53c0b567af456964d320b92481193 · Fix 403ac3c488d2f3d1a37ec16f279a16988b2d4e423a523c2e2a2b1b856d4a6b8c