mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-18 05:48:00 +00:00
fixup! Update, bugfixes on diff and edit handling
This commit is contained in:
@@ -69,13 +69,26 @@ func Load(workspaceRoot string) (*Config, error) {
|
||||
cfg.WorkspaceRoot = cwd
|
||||
}
|
||||
|
||||
// Try to load from config file in workspace root
|
||||
// Try to load from config file in workspace root.
|
||||
// Save WorkspaceRoot before loading config file so it cannot be overridden.
|
||||
savedRoot := cfg.WorkspaceRoot
|
||||
configPath := filepath.Join(cfg.WorkspaceRoot, ".mcp-filepuff.json")
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Restore WorkspaceRoot — config file must not override path guards.
|
||||
cfg.WorkspaceRoot = savedRoot
|
||||
|
||||
// Clamp size limits to prevent config file from requesting excessive memory.
|
||||
const maxAllowedSize int64 = 100 * 1024 * 1024 // 100 MB
|
||||
if cfg.MaxFileSize > maxAllowedSize {
|
||||
cfg.MaxFileSize = maxAllowedSize
|
||||
}
|
||||
if cfg.MaxParseSize > maxAllowedSize {
|
||||
cfg.MaxParseSize = maxAllowedSize
|
||||
}
|
||||
|
||||
// Override from environment variables
|
||||
cfg.loadFromEnv()
|
||||
@@ -173,8 +186,8 @@ func (c *Config) IsPathAllowed(path string) bool {
|
||||
|
||||
// Check if the path is within workspace (doesn't start with ..)
|
||||
// This prevents both "../" attacks and symlink bypasses
|
||||
// Also reject empty relative path (which means it's the workspace root itself)
|
||||
return rel != "." && !strings.HasPrefix(rel, "..")
|
||||
// The workspace root itself (rel == ".") is a valid, allowed path
|
||||
return !strings.HasPrefix(rel, "..")
|
||||
}
|
||||
|
||||
// Validate validates the configuration and returns an error if invalid.
|
||||
|
||||
@@ -389,8 +389,8 @@ func TestIsPathAllowedEdgeCases(t *testing.T) {
|
||||
{
|
||||
name: "workspace_root_itself",
|
||||
path: tmpDir,
|
||||
allowed: false,
|
||||
desc: "workspace root itself should not be allowed",
|
||||
allowed: true,
|
||||
desc: "workspace root itself should be allowed",
|
||||
},
|
||||
{
|
||||
name: "dot_relative",
|
||||
@@ -546,6 +546,59 @@ func TestConfigFileLoadingErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsPathAllowed_SymlinkOutsideWorkspace verifies that symlinks pointing
|
||||
// outside the workspace are rejected (T-01).
|
||||
func TestIsPathAllowed_SymlinkOutsideWorkspace(t *testing.T) {
|
||||
// Create two separate temp dirs: one as workspace, one as outside target
|
||||
workspace, err := os.MkdirTemp("", "mcp-workspace-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create workspace dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(workspace) })
|
||||
|
||||
outside, err := os.MkdirTemp("", "mcp-outside-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create outside dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(outside) })
|
||||
|
||||
// Create a file outside the workspace
|
||||
outsideFile := filepath.Join(outside, "secret.txt")
|
||||
if err := os.WriteFile(outsideFile, []byte("secret"), 0o600); err != nil {
|
||||
t.Fatalf("failed to write outside file: %v", err)
|
||||
}
|
||||
|
||||
// Create a symlink inside the workspace pointing outside
|
||||
symlinkPath := filepath.Join(workspace, "escape-link")
|
||||
if err := os.Symlink(outsideFile, symlinkPath); err != nil {
|
||||
t.Skip("symlink creation not supported on this system")
|
||||
}
|
||||
|
||||
cfg := Default()
|
||||
cfg.WorkspaceRoot = workspace
|
||||
|
||||
// The symlink resolves to a file outside workspace — must be rejected
|
||||
if cfg.IsPathAllowed(symlinkPath) {
|
||||
t.Error("symlink pointing outside workspace should NOT be allowed")
|
||||
}
|
||||
|
||||
// Direct access to the outside file should also be rejected
|
||||
if cfg.IsPathAllowed(outsideFile) {
|
||||
t.Error("file outside workspace should NOT be allowed")
|
||||
}
|
||||
|
||||
// File inside workspace should still be allowed
|
||||
insideFile := filepath.Join(workspace, "safe.txt")
|
||||
if !cfg.IsPathAllowed(insideFile) {
|
||||
t.Error("file inside workspace should be allowed")
|
||||
}
|
||||
|
||||
// Workspace root itself should be allowed (C-08 fix)
|
||||
if !cfg.IsPathAllowed(workspace) {
|
||||
t.Error("workspace root itself should be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a string contains a substring.
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
|
||||
@@ -126,7 +126,7 @@ func TestIsPathAllowed_BasicCases(t *testing.T) {
|
||||
{
|
||||
name: "workspace root itself",
|
||||
path: tmpDir,
|
||||
expected: false, // Empty relative path
|
||||
expected: true, // Workspace root is a valid, allowed path (needed for ast_query)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user