Skip to content

Commit b911e60

Browse files
committed
Add pathutil for path validation
1 parent eb8f208 commit b911e60

3 files changed

Lines changed: 147 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,16 +95,14 @@ jobs:
9595
run: go install golang.org/x/vuln/cmd/govulncheck@latest
9696

9797
- name: Run govulncheck and generate SARIF
98+
working-directory: ${{ github.workspace }}/..
9899
run: |
99-
# Generate SARIF output
100-
govulncheck -format sarif -scan=module ./... > govulncheck-results.sarif
101-
102-
# Also generate JSON for counting (optional)
100+
govulncheck -format sarif -scan=module > govulncheck-results.sarif
101+
103102
govulncheck -json ./... > vuln.json
104103
count=$(jq '[.[] | select(.finding != null and .finding.trace != null)] | length' vuln.json || echo 0)
105104
echo "Found $count vulnerabilities"
106-
107-
# Note: We don't fail the build here since we want to upload the SARIF file
105+
108106
if [ "$count" -gt 0 ]; then
109107
echo "⚠️ Vulnerabilities found by govulncheck (see Security tab for details)"
110108
else
@@ -194,7 +192,7 @@ jobs:
194192
actions: read
195193
contents: read
196194
security-events: write
197-
195+
198196
steps:
199197
- name: Checkout code
200198
uses: actions/checkout@v4

pkg/common/pathutil/pathutil.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package pathutil
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
)
8+
9+
// SafePath validates and constructs a safe file path within a base directory
10+
func SafePath(baseDir, filename string) (string, error) {
11+
// Clean the filename to prevent path traversal
12+
cleanFilename := filepath.Clean(filename)
13+
14+
// Check for path traversal attempts
15+
if strings.Contains(cleanFilename, "..") {
16+
return "", fmt.Errorf("invalid filename: path traversal not allowed")
17+
}
18+
19+
// Construct the full path
20+
fullPath := filepath.Join(baseDir, cleanFilename)
21+
22+
// Ensure the path is within the base directory
23+
absBase, err := filepath.Abs(baseDir)
24+
if err != nil {
25+
return "", fmt.Errorf("failed to get absolute path for base directory: %w", err)
26+
}
27+
28+
absPath, err := filepath.Abs(fullPath)
29+
if err != nil {
30+
return "", fmt.Errorf("failed to get absolute path: %w", err)
31+
}
32+
33+
if !strings.HasPrefix(absPath, absBase) {
34+
return "", fmt.Errorf("path outside base directory not allowed")
35+
}
36+
37+
return fullPath, nil
38+
}
39+
40+
// ValidateFilePath validates a file path for security concerns
41+
func ValidateFilePath(filePath string) error {
42+
// Clean the path
43+
cleanPath := filepath.Clean(filePath)
44+
45+
// Check for path traversal attempts
46+
if strings.Contains(cleanPath, "..") {
47+
return fmt.Errorf("invalid file path: path traversal not allowed")
48+
}
49+
50+
return nil
51+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package pathutil
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestValidateFilePath(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
path string
11+
wantErr bool
12+
}{
13+
{
14+
name: "valid simple path",
15+
path: "test.json",
16+
wantErr: false,
17+
},
18+
{
19+
name: "valid relative path",
20+
path: "config/test.json",
21+
wantErr: false,
22+
},
23+
{
24+
name: "path traversal attempt",
25+
path: "../../../etc/passwd",
26+
wantErr: true,
27+
},
28+
{
29+
name: "path traversal with clean",
30+
path: "config/../../../etc/passwd",
31+
wantErr: true,
32+
},
33+
{
34+
name: "valid absolute path",
35+
path: "/tmp/test.json",
36+
wantErr: false,
37+
},
38+
}
39+
40+
for _, tt := range tests {
41+
t.Run(tt.name, func(t *testing.T) {
42+
err := ValidateFilePath(tt.path)
43+
if (err != nil) != tt.wantErr {
44+
t.Errorf("ValidateFilePath() error = %v, wantErr %v", err, tt.wantErr)
45+
}
46+
})
47+
}
48+
}
49+
50+
func TestSafePath(t *testing.T) {
51+
tests := []struct {
52+
name string
53+
baseDir string
54+
filename string
55+
wantErr bool
56+
}{
57+
{
58+
name: "valid file in base dir",
59+
baseDir: "/tmp",
60+
filename: "test.json",
61+
wantErr: false,
62+
},
63+
{
64+
name: "path traversal attempt",
65+
baseDir: "/tmp",
66+
filename: "../../../etc/passwd",
67+
wantErr: true,
68+
},
69+
{
70+
name: "path traversal with clean",
71+
baseDir: "/tmp",
72+
filename: "config/../../../etc/passwd",
73+
wantErr: true,
74+
},
75+
{
76+
name: "valid subdirectory",
77+
baseDir: "/tmp",
78+
filename: "config/test.json",
79+
wantErr: false,
80+
},
81+
}
82+
83+
for _, tt := range tests {
84+
t.Run(tt.name, func(t *testing.T) {
85+
_, err := SafePath(tt.baseDir, tt.filename)
86+
if (err != nil) != tt.wantErr {
87+
t.Errorf("SafePath() error = %v, wantErr %v", err, tt.wantErr)
88+
}
89+
})
90+
}
91+
}

0 commit comments

Comments
 (0)