Skip to content

Commit cf14129

Browse files
claudeDavidVeksler
authored andcommitted
Fix: Address code review feedback - security, performance, and code quality
This commit addresses all feedback from PR #4's Copilot AI review: Security Fixes: - Add PathSecurity utility to prevent path traversal attacks - Validate all file paths in GetFileContent to prevent directory traversal - Replace empty catch blocks with specific exception types Performance Improvements: - Remove unnecessary Task.Run wrappers (synchronous I/O doesn't benefit) - Optimize ToLowerInvariant() calls to avoid repeated allocations on large strings - Cache lowercase conversions once per scoring operation Code Quality Improvements: - Replace magic numbers with named constants throughout * Scoring weights (FileNameWeight, FilePathWeight, etc.) * Scoring parameters (NeutralScore, MaxMatchesPerKeyword, etc.) * File importance boost values (ReadmeBoost, ConfigBoost, etc.) * Token reservation constants (StructureTokenReservation) - Fix misleading file path length heuristic * Now uses actual file size instead of path length * Properly penalizes large files (50KB+ threshold) Build Fixes: - Fix .NET 10.0 target framework error in test project (downgrade to .NET 9.0) Technical Details: - PathSecurity.ValidatePathWithinRoot ensures resolved paths stay within project - SecurityException thrown for path traversal attempts - Specific exception handling for UnauthorizedAccessException, IOException, DirectoryNotFoundException - Reduced token reservation from 2000 to 1000 for more reasonable small project handling - All magic numbers extracted to const fields with descriptive names
1 parent 5cb5990 commit cf14129

5 files changed

Lines changed: 143 additions & 35 deletions

File tree

CodeContext.Tests/CodeContext.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net10.0</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<IsPackable>false</IsPackable>

Mcp/CodeContextTools.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public CodeContextTools(IConsoleWriter console)
2727
/// </summary>
2828
[McpServerTool]
2929
[Description("Get optimized code context for a task. Intelligently selects most relevant files within token budget.")]
30-
public async Task<string> GetCodeContext(
30+
public string GetCodeContext(
3131
[Description("Path to the project directory to analyze")] string projectPath,
3232
[Description("Description of the task (e.g., 'fix authentication bug', 'add payment feature')")] string taskDescription,
3333
[Description("Maximum number of tokens to use (default: 50000)")] int tokenBudget = 50000,
@@ -58,8 +58,8 @@ public async Task<string> GetCodeContext(
5858
? s
5959
: TokenBudgetOptimizer.SelectionStrategy.ValueOptimized;
6060

61-
// Scan and score files
62-
var files = await Task.Run(() => GetAllProjectFiles(scanner, projectPath));
61+
// Scan and score files (synchronous I/O, no Task.Run needed)
62+
var files = GetAllProjectFiles(scanner, projectPath);
6363
var scoredFiles = files
6464
.Select(f => scorer.ScoreFile(f.path, f.content, taskDescription))
6565
.ToList();
@@ -152,7 +152,7 @@ public string GetProjectStructure(
152152
/// </summary>
153153
[McpServerTool]
154154
[Description("List all files in a project with token counts and basic metadata")]
155-
public async Task<string> ListProjectFiles(
155+
public string ListProjectFiles(
156156
[Description("Path to the project directory")] string projectPath,
157157
[Description("Optional query to filter/rank files")] string? query = null)
158158
{
@@ -170,7 +170,8 @@ public async Task<string> ListProjectFiles(
170170
var fileChecker = new FileFilterService(filterConfig, gitIgnoreParser);
171171
var scanner = new ProjectScanner(fileChecker, _console);
172172

173-
var files = await Task.Run(() => GetAllProjectFiles(scanner, projectPath));
173+
// Synchronous I/O, no Task.Run needed
174+
var files = GetAllProjectFiles(scanner, projectPath);
174175

175176
var output = new StringBuilder();
176177
output.AppendLine($"# Project Files: {Path.GetFileName(projectPath)}");
@@ -240,7 +241,14 @@ public string GetFileContent(
240241

241242
foreach (var relativePath in paths)
242243
{
243-
var fullPath = Path.Combine(projectPath, relativePath);
244+
// Validate path to prevent path traversal attacks
245+
if (!PathSecurity.TryValidatePathWithinRoot(projectPath, relativePath, out var fullPath))
246+
{
247+
output.AppendLine($"## {relativePath}");
248+
output.AppendLine("❌ Security error: Path traversal detected");
249+
output.AppendLine();
250+
continue;
251+
}
244252

245253
if (!File.Exists(fullPath))
246254
{
@@ -312,16 +320,24 @@ private static void CollectFiles(
312320
var relativePath = Path.GetRelativePath(rootPath, entry);
313321
files.Add((relativePath, content));
314322
}
315-
catch
323+
catch (UnauthorizedAccessException)
316324
{
317-
// Skip files that can't be read
325+
// Skip files with permission issues
326+
}
327+
catch (IOException)
328+
{
329+
// Skip files that are locked or in use
318330
}
319331
}
320332
}
321333
}
322-
catch
334+
catch (UnauthorizedAccessException)
335+
{
336+
// Skip directories with permission issues
337+
}
338+
catch (DirectoryNotFoundException)
323339
{
324-
// Skip directories that can't be accessed
340+
// Skip if directory was deleted during scan
325341
}
326342
}
327343
}

Services/FileRelevanceScorer.cs

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ namespace CodeContext.Services;
99
/// </summary>
1010
public class FileRelevanceScorer
1111
{
12+
// Scoring weights for different factors
13+
private const double FileNameWeight = 0.30;
14+
private const double FilePathWeight = 0.20;
15+
private const double ContentWeight = 0.40;
16+
private const double ImportanceWeight = 0.10;
17+
18+
// Scoring parameters
19+
private const double NeutralScore = 0.5;
20+
private const double LowDefaultScore = 0.3;
21+
private const int MaxMatchesPerKeyword = 10;
22+
private const int ContentLengthNormalizer = 100;
23+
24+
// File importance boost values
25+
private const double ReadmeBoost = 0.3;
26+
private const double ConfigBoost = 0.2;
27+
private const double MainFileBoost = 0.2;
28+
private const double IndexFileBoost = 0.15;
29+
private const double TestFileBoost = 0.1;
30+
1231
private readonly string _projectPath;
1332

1433
public FileRelevanceScorer(string projectPath)
@@ -52,15 +71,15 @@ public ScoredFile ScoreFile(string filePath, string content, string query)
5271
breakdown["content"] = contentScore;
5372

5473
// 4. File importance indicators (10% weight)
55-
var importanceScore = ScoreImportance(filePath);
74+
var importanceScore = ScoreImportance(filePath, content.Length);
5675
breakdown["importance"] = importanceScore;
5776

5877
// Calculate weighted total score
5978
var totalScore =
60-
(nameScore * 0.30) +
61-
(pathScore * 0.20) +
62-
(contentScore * 0.40) +
63-
(importanceScore * 0.10);
79+
(nameScore * FileNameWeight) +
80+
(pathScore * FilePathWeight) +
81+
(contentScore * ContentWeight) +
82+
(importanceScore * ImportanceWeight);
6483

6584
var tokenCount = TokenCounter.EstimateTokensForFile(filePath, content);
6685

@@ -96,10 +115,10 @@ private static List<string> ExtractKeywords(string query)
96115
/// </summary>
97116
private static double ScoreFileName(string filePath, List<string> keywords)
98117
{
99-
var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant();
100118
if (keywords.Count == 0)
101-
return 0.5; // Neutral score if no keywords
119+
return NeutralScore;
102120

121+
var fileName = Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant();
103122
var matchCount = keywords.Count(keyword => fileName.Contains(keyword));
104123
return Math.Min(1.0, matchCount / (double)keywords.Count * 1.5);
105124
}
@@ -109,58 +128,65 @@ private static double ScoreFileName(string filePath, List<string> keywords)
109128
/// </summary>
110129
private static double ScoreFilePath(string filePath, List<string> keywords)
111130
{
112-
var pathLower = filePath.ToLowerInvariant();
113131
if (keywords.Count == 0)
114-
return 0.5;
132+
return NeutralScore;
115133

134+
var pathLower = filePath.ToLowerInvariant();
116135
var matchCount = keywords.Count(keyword => pathLower.Contains(keyword));
117136
return Math.Min(1.0, matchCount / (double)keywords.Count);
118137
}
119138

120139
/// <summary>
121140
/// Scores based on content relevance.
141+
/// Optimized to avoid repeated ToLowerInvariant() calls on large content.
122142
/// </summary>
123143
private static double ScoreContent(string content, List<string> keywords)
124144
{
125145
if (string.IsNullOrWhiteSpace(content) || keywords.Count == 0)
126-
return 0.3; // Low default score
146+
return LowDefaultScore;
127147

148+
// Cache lowercase conversion once to avoid repeated allocations
128149
var contentLower = content.ToLowerInvariant();
150+
129151
var totalMatches = keywords.Sum(keyword =>
130152
{
131153
var count = Regex.Matches(contentLower, Regex.Escape(keyword)).Count;
132-
return Math.Min(count, 10); // Cap at 10 matches per keyword to avoid skew
154+
return Math.Min(count, MaxMatchesPerKeyword);
133155
});
134156

135157
// Normalize by content length and keyword count
136-
var density = totalMatches / (double)(content.Length / 100 + 1);
158+
var density = totalMatches / (double)(content.Length / ContentLengthNormalizer + 1);
137159
return Math.Min(1.0, density * keywords.Count);
138160
}
139161

140162
/// <summary>
141163
/// Scores based on file importance indicators.
142164
/// </summary>
143-
private static double ScoreImportance(string filePath)
165+
/// <param name="filePath">Path to the file.</param>
166+
/// <param name="fileSize">Size of the file content in bytes.</param>
167+
private static double ScoreImportance(string filePath, int fileSize)
144168
{
169+
const int VeryLargeFileThreshold = 50000; // 50KB
170+
const double LargeFilePenalty = 0.1;
171+
145172
var fileName = Path.GetFileName(filePath).ToLowerInvariant();
146-
var score = 0.5; // Base score
173+
var score = NeutralScore;
147174

148175
// Boost for important file types
149176
if (fileName.Contains("readme"))
150-
score += 0.3;
177+
score += ReadmeBoost;
151178
if (fileName.Contains("config") || fileName.Contains("settings"))
152-
score += 0.2;
179+
score += ConfigBoost;
153180
if (fileName.Contains("main") || fileName.Contains("program") || fileName.Contains("app"))
154-
score += 0.2;
181+
score += MainFileBoost;
155182
if (fileName.Contains("index") || fileName.Contains("router"))
156-
score += 0.15;
183+
score += IndexFileBoost;
157184
if (fileName.Contains("test") || fileName.Contains("spec"))
158-
score += 0.1; // Tests are useful but secondary
185+
score += TestFileBoost;
159186

160-
// Penalize very long files (might be generated/verbose)
161-
// This would need actual file size, using path length as proxy
162-
if (filePath.Length > 100)
163-
score -= 0.1;
187+
// Penalize very large files (might be generated/verbose)
188+
if (fileSize > VeryLargeFileThreshold)
189+
score -= LargeFilePenalty;
164190

165191
return Math.Clamp(score, 0.0, 1.0);
166192
}

Services/TokenBudgetOptimizer.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ namespace CodeContext.Services;
1010
/// </summary>
1111
public class TokenBudgetOptimizer
1212
{
13+
// Token reservation constants
14+
private const int StructureTokenReservation = 1000; // Reasonable estimate for project structure
15+
private const int MinimalOverhead = 100; // Minimal overhead for formatting
16+
1317
public enum SelectionStrategy
1418
{
1519
/// <summary>
@@ -69,7 +73,7 @@ public OptimizationResult OptimizeSelection(
6973
}
7074

7175
// Reserve tokens for project structure if requested
72-
var reservedTokens = includeStructure ? 2000 : 100; // Structure + overhead
76+
var reservedTokens = includeStructure ? StructureTokenReservation : MinimalOverhead;
7377
var availableBudget = Math.Max(0, tokenBudget - reservedTokens);
7478

7579
var selected = strategy switch

Utils/PathSecurity.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
namespace CodeContext.Utils;
2+
3+
/// <summary>
4+
/// Security utilities for path validation to prevent path traversal attacks.
5+
/// </summary>
6+
public static class PathSecurity
7+
{
8+
/// <summary>
9+
/// Validates that a resolved path is within the allowed root directory.
10+
/// Prevents path traversal attacks using ".." or absolute paths.
11+
/// </summary>
12+
/// <param name="rootPath">The root directory that paths must be within.</param>
13+
/// <param name="relativePath">The relative path to validate.</param>
14+
/// <returns>The validated full path if safe, null if path traversal detected.</returns>
15+
/// <exception cref="SecurityException">Thrown when path traversal is detected.</exception>
16+
public static string ValidatePathWithinRoot(string rootPath, string relativePath)
17+
{
18+
Guard.NotNullOrEmpty(rootPath, nameof(rootPath));
19+
Guard.NotNullOrEmpty(relativePath, nameof(relativePath));
20+
21+
// Get absolute paths for comparison
22+
var absoluteRoot = Path.GetFullPath(rootPath);
23+
var combinedPath = Path.Combine(absoluteRoot, relativePath);
24+
var absoluteCombined = Path.GetFullPath(combinedPath);
25+
26+
// Ensure the resolved path is within the root directory
27+
if (!absoluteCombined.StartsWith(absoluteRoot, StringComparison.OrdinalIgnoreCase))
28+
{
29+
throw new SecurityException(
30+
$"Path traversal detected: '{relativePath}' resolves outside root directory. " +
31+
$"Root: {absoluteRoot}, Resolved: {absoluteCombined}");
32+
}
33+
34+
return absoluteCombined;
35+
}
36+
37+
/// <summary>
38+
/// Tries to validate a path, returning false if path traversal is detected.
39+
/// </summary>
40+
public static bool TryValidatePathWithinRoot(string rootPath, string relativePath, out string? validatedPath)
41+
{
42+
try
43+
{
44+
validatedPath = ValidatePathWithinRoot(rootPath, relativePath);
45+
return true;
46+
}
47+
catch (SecurityException)
48+
{
49+
validatedPath = null;
50+
return false;
51+
}
52+
}
53+
}
54+
55+
/// <summary>
56+
/// Exception thrown when a security violation is detected.
57+
/// </summary>
58+
public class SecurityException : Exception
59+
{
60+
public SecurityException(string message) : base(message) { }
61+
public SecurityException(string message, Exception innerException) : base(message, innerException) { }
62+
}

0 commit comments

Comments
 (0)