From a1c9e16abe068a56bc817d8fcead8b9a37dd77a4 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Fri, 12 Jun 2026 16:06:06 +0200 Subject: [PATCH] Fixed up so trying to read (from script) symlinks outside project folder result in error --- internal/compiler/scriptexec.go | 9 ++++++ internal/compiler/scriptexec_test.go | 44 ++++++++++++++++++++++++++++ main.go | 6 +++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/internal/compiler/scriptexec.go b/internal/compiler/scriptexec.go index 37db8fe..c9a8152 100644 --- a/internal/compiler/scriptexec.go +++ b/internal/compiler/scriptexec.go @@ -314,6 +314,15 @@ func validateScriptFilePath(projectRoot, path string) (string, error) { return "", fmt.Errorf("file access denied: path resolves outside project folder") } + // Resolve symlinks to prevent symlink-based escape + realPath, err := filepath.EvalSymlinks(resolved) + if err == nil { + if !strings.HasPrefix(realPath, projectRootWithSep) && realPath != projectRoot { + return "", fmt.Errorf("file access denied: symlink target resolves outside project folder") + } + return realPath, nil + } + return resolved, nil } diff --git a/internal/compiler/scriptexec_test.go b/internal/compiler/scriptexec_test.go index 8a2211b..91eab3e 100644 --- a/internal/compiler/scriptexec_test.go +++ b/internal/compiler/scriptexec_test.go @@ -501,6 +501,50 @@ func TestValidateScriptFilePath_RejectsEmptyPath(t *testing.T) { } } +func TestValidateScriptFilePath_RejectsSymlinkEscape(t *testing.T) { + projectRoot := t.TempDir() + + // Symlink pointing outside the project root + outsideTarget := filepath.Join(projectRoot, "..", "outside.txt") + if err := os.WriteFile(outsideTarget, []byte("secret"), 0644); err != nil { + t.Fatal(err) + } + + symlinkPath := filepath.Join(projectRoot, "evil_link") + if err := os.Symlink(outsideTarget, symlinkPath); err != nil { + t.Fatal(err) + } + + _, err := validateScriptFilePath(projectRoot, "evil_link") + if err == nil { + t.Error("expected error for symlink escaping project root, got nil") + } +} + +func TestValidateScriptFilePath_AllowsSymlinkWithinProject(t *testing.T) { + projectRoot := t.TempDir() + + // Create a file inside the project + targetPath := filepath.Join(projectRoot, "real_file.txt") + if err := os.WriteFile(targetPath, []byte("data"), 0644); err != nil { + t.Fatal(err) + } + + // Create a symlink pointing inside the project + symlinkPath := filepath.Join(projectRoot, "link_to_real.txt") + if err := os.Symlink("real_file.txt", symlinkPath); err != nil { + t.Fatal(err) + } + + got, err := validateScriptFilePath(projectRoot, "link_to_real.txt") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != targetPath { + t.Errorf("got %q, want %q", got, targetPath) + } +} + // --- Tests for load_binary builtin --- func TestExecuteScript_LoadBinary(t *testing.T) { diff --git a/main.go b/main.go index 279279a..140b4ad 100644 --- a/main.go +++ b/main.go @@ -143,7 +143,11 @@ func compileOnly(inFile, outFile string, opt, optDebug, optC64 bool, optExcludes if err != nil { return fmt.Errorf("failed to resolve input file path: %w", err) } - comp.Context().ProjectRoot = filepath.Dir(absInput) + projectRoot := filepath.Dir(absInput) + if canonicalRoot, err := filepath.EvalSymlinks(projectRoot); err == nil { + projectRoot = canonicalRoot + } + comp.Context().ProjectRoot = projectRoot comp.CmdlineOpt = opt comp.CmdlineDebug = optDebug // Build I/O regions from CLI flags