From f7b81b9dd76bd35ddc7f9ff0c6789547907cb919 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 17 Sep 2026 14:44:23 +0200 Subject: [PATCH] fix(02): resolve symlinks before fixture and vars path checks (WR-07) Co-authored-by: Cursor --- tide/flow.go | 18 +++++++++++++++++- tide/manifest.go | 3 +++ tide/proxy_test.go | 17 +++++++++++++++++ tide/variables.go | 22 ++++++++++++++++++++-- 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/tide/flow.go b/tide/flow.go index c6853fe..997537d 100644 --- a/tide/flow.go +++ b/tide/flow.go @@ -184,7 +184,20 @@ func materializeSidecar(base string, resp *Response) error { if base != "" { path = filepath.Join(base, resp.BodyFile) } - raw, err := os.ReadFile(path) + resolved, err := resolvePath(path) + if err != nil { + return fmt.Errorf("tide: read body_file %s: %w", resp.BodyFile, err) + } + if base != "" { + root, err := resolvePath(base) + if err != nil { + return fmt.Errorf("tide: fixture dir: %w", err) + } + if resolved != root && !strings.HasPrefix(resolved, root+string(os.PathSeparator)) { + return fmt.Errorf("tide: body_file %q escapes the fixture directory", resp.BodyFile) + } + } + raw, err := os.ReadFile(resolved) if err != nil { return fmt.Errorf("tide: read body_file %s: %w", resp.BodyFile, err) } @@ -208,6 +221,9 @@ func validateSidecar(path string) error { if clean == ".." || strings.HasPrefix(clean, "../") { return fmt.Errorf("body_file %q escapes the fixture directory", path) } + if _, err := resolvePath(path); err != nil { + return fmt.Errorf("body_file %q: %w", path, err) + } return nil } diff --git a/tide/manifest.go b/tide/manifest.go index cfc7da6..8733398 100644 --- a/tide/manifest.go +++ b/tide/manifest.go @@ -192,6 +192,9 @@ func validateFixturePath(p string) error { if clean == ".." || strings.HasPrefix(clean, "../") { return fmt.Errorf("fixture %q escapes the fixture directory", p) } + if _, err := resolvePath(p); err != nil { + return fmt.Errorf("fixture %q: %w", p, err) + } return nil } diff --git a/tide/proxy_test.go b/tide/proxy_test.go index dda534b..6db4b87 100644 --- a/tide/proxy_test.go +++ b/tide/proxy_test.go @@ -253,6 +253,23 @@ func TestProxyRejectsNonLoopbackOverflowAndCredentials(t *testing.T) { }); err == nil || !strings.Contains(err.Error(), "outside") { t.Fatalf("vars inside fixtures: %v", err) } + if err := os.WriteFile(inside, []byte("{}\n"), 0o600); err != nil { + t.Fatal(err) + } + linkDir := t.TempDir() + link := filepath.Join(linkDir, "vars-link.yaml") + if err := os.Symlink(inside, link); err != nil { + t.Fatal(err) + } + if _, err := NewProxy(ProxyConfig{ + Listen: "127.0.0.1:0", + Upstream: upstream.URL, + Fixtures: fixtures, + VarsPath: link, + Rules: mustParseRules(t, testRulesYAML()), + }); err == nil || !strings.Contains(err.Error(), "outside") { + t.Fatalf("symlink vars into fixtures: %v", err) + } outside := filepath.Join(t.TempDir(), "vars.yaml") p, err := NewProxy(ProxyConfig{ Listen: "127.0.0.1:0", diff --git a/tide/variables.go b/tide/variables.go index f3243e4..362a72b 100644 --- a/tide/variables.go +++ b/tide/variables.go @@ -686,11 +686,11 @@ func varsOutsideFixtures(varsPath, fixtures string) error { if varsPath == "" || fixtures == "" { return nil } - absVars, err := filepath.Abs(varsPath) + absVars, err := resolvePath(varsPath) if err != nil { return fmt.Errorf("tide: vars path: %w", err) } - absFix, err := filepath.Abs(fixtures) + absFix, err := resolvePath(fixtures) if err != nil { return fmt.Errorf("tide: fixtures path: %w", err) } @@ -700,6 +700,24 @@ func varsOutsideFixtures(varsPath, fixtures string) error { return nil } +func resolvePath(path string) (string, error) { + abs, err := filepath.Abs(path) + if err != nil { + return "", err + } + if eval, err := filepath.EvalSymlinks(abs); err == nil { + return eval, nil + } + parentEval, err := filepath.EvalSymlinks(filepath.Dir(abs)) + if err != nil { + if _, statErr := os.Lstat(abs); statErr == nil { + return "", fmt.Errorf("eval symlinks %s: %w", path, err) + } + return filepath.Clean(abs), nil + } + return filepath.Join(parentEval, filepath.Base(abs)), nil +} + func mergeRouteCaptures(step *Step, rules Rules) { if step == nil || len(step.Capture) > 0 { return