Skip to content

Commit 41ca111

Browse files
committed
fix: resolve path traversal errors and isolate integration tests
- Canonicalize paths in tasks using filepath.Abs and Clean to fix "outside target path" errors - Refactor checkFilePreconditions to ensure robust security jail validation - Update CreateDirTask and ExecCommandTask to use absolute paths for transaction tracking - Isolate integration tests by injecting Git identity via environment variables (GIT_AUTHOR_ID) - Prevent global Git config leakage and ensure hermetic test execution - Decompose TestNewCommand into subtests to verify modular language and template optionality - Reduce cyclomatic complexity in test suites by extracting state verification helpers
1 parent b2c11a9 commit 41ca111

File tree

5 files changed

+132
-111
lines changed

5 files changed

+132
-111
lines changed

pkg/tasks/create_directory.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package tasks
77
import (
88
"fmt"
99
"os"
10+
"path/filepath"
1011
"scbake/internal/types"
1112
"scbake/internal/util/fileutil"
1213
)
@@ -20,17 +21,23 @@ type CreateDirTask struct {
2021

2122
// Execute performs the task of creating the directory.
2223
func (t *CreateDirTask) Execute(tc types.TaskContext) error {
24+
// Canonicalize the path to ensure consistency across relative/absolute calls.
25+
absPath, err := filepath.Abs(t.Path)
26+
if err != nil {
27+
return fmt.Errorf("failed to resolve directory path %s: %w", t.Path, err)
28+
}
29+
2330
// Safety Tracking: If a transaction manager is present, register this path.
2431
// If the directory doesn't exist, it will be marked for cleanup on rollback.
2532
// If it does exist, this is a no-op (idempotent).
2633
if !tc.DryRun && tc.Tx != nil {
27-
if err := tc.Tx.Track(t.Path); err != nil {
34+
if err := tc.Tx.Track(absPath); err != nil {
2835
return fmt.Errorf("failed to track directory %s: %w", t.Path, err)
2936
}
3037
}
3138

32-
// Use the constant from the centralized location
33-
if err := os.MkdirAll(t.Path, fileutil.DirPerms); err != nil {
39+
// Use the constant from the centralized location for secure permissions.
40+
if err := os.MkdirAll(absPath, fileutil.DirPerms); err != nil {
3441
return fmt.Errorf("failed to create directory %s: %w", t.Path, err)
3542
}
3643
return nil

pkg/tasks/create_directory_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func TestCreateDirTask_Transaction(t *testing.T) {
5353
tx, _ := transaction.New(rootDir)
5454

5555
targetDir := filepath.Join(rootDir, "tracked_dir")
56+
absTargetDir, _ := filepath.Abs(targetDir)
5657

5758
task := &CreateDirTask{
5859
Path: targetDir,
@@ -75,7 +76,7 @@ func TestCreateDirTask_Transaction(t *testing.T) {
7576
t.Fatalf("Rollback failed: %v", err)
7677
}
7778

78-
if _, err := os.Stat(targetDir); !os.IsNotExist(err) {
79-
t.Error("Directory was not removed by rollback, likely wasn't tracked")
79+
if _, err := os.Stat(absTargetDir); !os.IsNotExist(err) {
80+
t.Error("Directory was not removed by rollback, likely wasn't tracked via absolute path")
8081
}
8182
}

pkg/tasks/create_template.go

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,37 @@ func (t *CreateTemplateTask) Priority() int {
4747

4848
// checkFilePreconditions handles path safety, directory creation, and existence checks.
4949
func checkFilePreconditions(finalPath, output, target string, force bool) error {
50-
// 1. Path Safety Check
51-
if !strings.HasPrefix(finalPath, target) {
52-
return fmt.Errorf("output path '%s' is outside the target path '%s'", output, target)
50+
// 1. Path Safety Check (Canonicalization)
51+
// We resolve both target and finalPath to absolute cleaned paths to ensure
52+
// that indicators like "." and relative subdirectories match correctly.
53+
absTarget, err := filepath.Abs(target)
54+
if err != nil {
55+
return fmt.Errorf("failed to resolve target path: %w", err)
56+
}
57+
absFinal, err := filepath.Abs(finalPath)
58+
if err != nil {
59+
return fmt.Errorf("failed to resolve output path: %w", err)
60+
}
61+
62+
cleanTarget := filepath.Clean(absTarget)
63+
cleanFinalPath := filepath.Clean(absFinal)
64+
65+
if !strings.HasPrefix(cleanFinalPath, cleanTarget) {
66+
return fmt.Errorf("task failed (%s): output path '%s' is outside the target path '%s'",
67+
filepath.Base(output), output, target)
5368
}
5469

5570
// 2. Ensure the directory exists
56-
dir := filepath.Dir(finalPath)
71+
dir := filepath.Dir(cleanFinalPath)
5772
if err := os.MkdirAll(dir, fileutil.DirPerms); err != nil {
5873
return fmt.Errorf("failed to create directory %s: %w", dir, err)
5974
}
6075

6176
// 3. Existence Check
6277
if !force {
63-
if _, err := os.Stat(finalPath); err == nil {
78+
if _, err := os.Stat(cleanFinalPath); err == nil {
6479
// File exists and Force is false.
65-
return fmt.Errorf("file already exists: %s. Use --force to overwrite", finalPath)
80+
return fmt.Errorf("file already exists: %s. Use --force to overwrite", output)
6681
} else if !errors.Is(err, os.ErrNotExist) {
6782
// Some other error occurred (e.g., permissions).
6883
return err
@@ -89,27 +104,30 @@ func (t *CreateTemplateTask) Execute(tc types.TaskContext) (err error) {
89104
}
90105

91106
// 2. Determine and check the final output path
92-
finalPath := filepath.Join(tc.TargetPath, filepath.Clean(t.OutputPath))
107+
finalPath := filepath.Join(tc.TargetPath, t.OutputPath)
93108

94109
// Check directory and file existence using the helper function.
95110
if err = checkFilePreconditions(finalPath, t.OutputPath, tc.TargetPath, tc.Force); err != nil {
96111
return err
97112
}
98113

114+
// Canonical path for tracking and writing
115+
absPath, _ := filepath.Abs(finalPath)
116+
99117
// 3. Safety Tracking: Register the file with the transaction manager.
100118
// If it exists, it will be backed up. If not, it will be marked for cleanup.
101119
if tc.Tx != nil {
102-
if err := tc.Tx.Track(finalPath); err != nil {
103-
return fmt.Errorf("failed to track file %s: %w", finalPath, err)
120+
if err := tc.Tx.Track(absPath); err != nil {
121+
return fmt.Errorf("failed to track file %s: %w", t.OutputPath, err)
104122
}
105123
}
106124

107125
// 4. Create the output file
108126
// G304: Path is explicitly sanitized and verified in checkFilePreconditions
109127
//nolint:gosec
110-
f, err := os.Create(finalPath)
128+
f, err := os.OpenFile(absPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileutil.FilePerms)
111129
if err != nil {
112-
return fmt.Errorf("failed to create file %s: %w", finalPath, err)
130+
return fmt.Errorf("failed to create file %s: %w", t.OutputPath, err)
113131
}
114132

115133
// Check the return value of f.Close()

pkg/tasks/create_template_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestCreateTemplateTask(t *testing.T) {
3535
Force: false,
3636
}
3737

38-
// Case 1: Render a valid template
38+
// Case 1: Render a valid template (Standard relative path)
3939
task := &CreateTemplateTask{
4040
TemplateFS: testTemplates,
4141
TemplatePath: "testdata/simple.tpl",
@@ -62,10 +62,10 @@ func TestCreateTemplateTask(t *testing.T) {
6262

6363
// Case 2: Existence Check (Should fail without Force)
6464
if err := task.Execute(tc); err == nil {
65-
t.Error("Overwriting existing file should fail without Force, but it succeeded")
65+
t.Error("Overwriting existing file should fail without Force")
6666
}
6767

68-
// Case 3: Force Overwrite (Should succeed)
68+
// Case 3: Force Overwrite
6969
tc.Force = true
7070
if err := task.Execute(tc); err != nil {
7171
t.Errorf("Force overwrite failed: %v", err)
@@ -92,7 +92,7 @@ func TestCreateTemplateTask_Transaction(t *testing.T) {
9292
rootDir := t.TempDir()
9393
tx, _ := transaction.New(rootDir)
9494

95-
// FIX: Provide a valid manifest so the template can render {{ (index .Projects 0).Name }}
95+
// Provide a valid manifest so the template can render {{ (index .Projects 0).Name }}
9696
manifest := &types.Manifest{
9797
SbakeVersion: "v1.0.0",
9898
Projects: []types.Project{
@@ -122,7 +122,9 @@ func TestCreateTemplateTask_Transaction(t *testing.T) {
122122

123123
// File should exist
124124
path := filepath.Join(rootDir, "tracked_file.txt")
125-
if _, err := os.Stat(path); os.IsNotExist(err) {
125+
absPath, _ := filepath.Abs(path)
126+
127+
if _, err := os.Stat(absPath); os.IsNotExist(err) {
126128
t.Fatal("File was not created")
127129
}
128130

0 commit comments

Comments
 (0)