fix(02): resolve symlinks before fixture and vars path checks (WR-07)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-17 14:44:23 +02:00
parent 0ac9dc45d0
commit f7b81b9dd7
4 changed files with 57 additions and 3 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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