fix(config): clamp max_edit_size and max_search_results

Only max_file_size and max_parse_size were clamped after reading .mcp-filepuff.json; max_edit_size and max_search_results were unbounded, letting a config file request excessive memory. Clamp max_edit_size to 100MB and max_search_results to 100k (and floor negatives to 0). Adds TestLoadClampsExcessiveLimits.
This commit is contained in:
2026-06-20 00:33:34 +01:00
parent 5452cfce30
commit 96db1c7749
2 changed files with 40 additions and 1 deletions
+11 -1
View File
@@ -84,14 +84,24 @@ func Load(workspaceRoot string) (*Config, error) {
// Restore WorkspaceRoot — config file must not override path guards. // Restore WorkspaceRoot — config file must not override path guards.
cfg.WorkspaceRoot = savedRoot cfg.WorkspaceRoot = savedRoot
// Clamp size limits to prevent config file from requesting excessive memory. // Clamp size/count limits so the config file cannot request excessive memory.
const maxAllowedSize int64 = 100 * 1024 * 1024 // 100 MB const maxAllowedSize int64 = 100 * 1024 * 1024 // 100 MB
const maxAllowedSearchResults = 100000
if cfg.MaxFileSize > maxAllowedSize { if cfg.MaxFileSize > maxAllowedSize {
cfg.MaxFileSize = maxAllowedSize cfg.MaxFileSize = maxAllowedSize
} }
if cfg.MaxParseSize > maxAllowedSize { if cfg.MaxParseSize > maxAllowedSize {
cfg.MaxParseSize = maxAllowedSize cfg.MaxParseSize = maxAllowedSize
} }
if cfg.MaxEditSize > maxAllowedSize {
cfg.MaxEditSize = maxAllowedSize
}
if cfg.MaxSearchResults > maxAllowedSearchResults {
cfg.MaxSearchResults = maxAllowedSearchResults
}
if cfg.MaxSearchResults < 0 {
cfg.MaxSearchResults = 0 // negative would re-enable unbounded search
}
// Override from environment variables // Override from environment variables
cfg.loadFromEnv() cfg.loadFromEnv()
+29
View File
@@ -183,6 +183,35 @@ func TestLoadWithConfigFile(t *testing.T) {
} }
} }
// A config file requesting excessive size/count limits must be clamped so it
// cannot force the server into unbounded memory use.
func TestLoadClampsExcessiveLimits(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, ".mcp-filepuff.json")
configContent := `{
"max_file_size": 999999999999,
"max_parse_size": 999999999999,
"max_edit_size": 999999999999,
"max_search_results": 999999999
}`
if err := os.WriteFile(configPath, []byte(configContent), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
cfg, err := Load(tmpDir)
if err != nil {
t.Fatalf("Load failed: %v", err)
}
const maxSize int64 = 100 * 1024 * 1024
if cfg.MaxFileSize > maxSize || cfg.MaxParseSize > maxSize || cfg.MaxEditSize > maxSize {
t.Errorf("size limits not clamped: file=%d parse=%d edit=%d", cfg.MaxFileSize, cfg.MaxParseSize, cfg.MaxEditSize)
}
if cfg.MaxSearchResults > 100000 {
t.Errorf("MaxSearchResults not clamped: %d", cfg.MaxSearchResults)
}
}
// TestValidate tests the Validate method with various inputs. // TestValidate tests the Validate method with various inputs.
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
tests := []struct { tests := []struct {