66 "io"
77 "os"
88 "path/filepath"
9+ "strings"
910)
1011
1112type LocalProvider struct {
@@ -19,18 +20,62 @@ func NewLocalProvider(cfg Config) (*LocalProvider, error) {
1920 basePath = "./storage"
2021 }
2122
22- if err := os .MkdirAll (basePath , 0755 ); err != nil {
23+ // Get absolute path for base directory
24+ absBasePath , err := filepath .Abs (basePath )
25+ if err != nil {
26+ return nil , fmt .Errorf ("failed to resolve base path: %w" , err )
27+ }
28+
29+ if err := os .MkdirAll (absBasePath , 0755 ); err != nil {
2330 return nil , fmt .Errorf ("failed to create storage directory: %w" , err )
2431 }
2532
2633 return & LocalProvider {
27- basePath : basePath ,
34+ basePath : absBasePath ,
2835 baseURL : cfg .BaseURL ,
2936 }, nil
3037}
3138
39+ // securePath validates and returns a safe file path within the base directory.
40+ // Returns an error if the path would escape the base directory.
41+ func (l * LocalProvider ) securePath (key string ) (string , error ) {
42+ // Clean the key to remove any .. or other traversal attempts
43+ cleanKey := filepath .Clean (key )
44+
45+ // Join with base path
46+ filePath := filepath .Join (l .basePath , cleanKey )
47+
48+ // Get absolute path to resolve any remaining traversal
49+ absPath , err := filepath .Abs (filePath )
50+ if err != nil {
51+ return "" , fmt .Errorf ("failed to resolve path: %w" , err )
52+ }
53+
54+ // Verify the resolved path is within the base directory
55+ // Use filepath.Clean on basePath to ensure consistent comparison
56+ if ! strings .HasPrefix (absPath , l .basePath + string (filepath .Separator )) && absPath != l .basePath {
57+ return "" , fmt .Errorf ("path traversal detected: access denied" )
58+ }
59+
60+ // Check for symlinks that might escape the base directory
61+ // Only check if the path exists (for read operations)
62+ if _ , err := os .Lstat (absPath ); err == nil {
63+ realPath , err := filepath .EvalSymlinks (absPath )
64+ if err == nil {
65+ if ! strings .HasPrefix (realPath , l .basePath + string (filepath .Separator )) && realPath != l .basePath {
66+ return "" , fmt .Errorf ("symlink escape detected: access denied" )
67+ }
68+ }
69+ }
70+
71+ return absPath , nil
72+ }
73+
3274func (l * LocalProvider ) Upload (ctx context.Context , key string , reader io.Reader , size int64 ) error {
33- filePath := filepath .Join (l .basePath , key )
75+ filePath , err := l .securePath (key )
76+ if err != nil {
77+ return err
78+ }
3479
3580 if err := os .MkdirAll (filepath .Dir (filePath ), 0755 ); err != nil {
3681 return fmt .Errorf ("failed to create directory: %w" , err )
@@ -50,7 +95,11 @@ func (l *LocalProvider) Upload(ctx context.Context, key string, reader io.Reader
5095}
5196
5297func (l * LocalProvider ) Download (ctx context.Context , key string ) (io.ReadCloser , error ) {
53- filePath := filepath .Join (l .basePath , key )
98+ filePath , err := l .securePath (key )
99+ if err != nil {
100+ return nil , err
101+ }
102+
54103 file , err := os .Open (filePath )
55104 if err != nil {
56105 return nil , fmt .Errorf ("failed to open file: %w" , err )
@@ -59,16 +108,24 @@ func (l *LocalProvider) Download(ctx context.Context, key string) (io.ReadCloser
59108}
60109
61110func (l * LocalProvider ) Delete (ctx context.Context , key string ) error {
62- filePath := filepath .Join (l .basePath , key )
111+ filePath , err := l .securePath (key )
112+ if err != nil {
113+ return err
114+ }
115+
63116 if err := os .Remove (filePath ); err != nil {
64117 return fmt .Errorf ("failed to delete file: %w" , err )
65118 }
66119 return nil
67120}
68121
69122func (l * LocalProvider ) Exists (ctx context.Context , key string ) (bool , error ) {
70- filePath := filepath .Join (l .basePath , key )
71- _ , err := os .Stat (filePath )
123+ filePath , err := l .securePath (key )
124+ if err != nil {
125+ return false , err
126+ }
127+
128+ _ , err = os .Stat (filePath )
72129 if err != nil {
73130 if os .IsNotExist (err ) {
74131 return false , nil
@@ -80,9 +137,16 @@ func (l *LocalProvider) Exists(ctx context.Context, key string) (bool, error) {
80137
81138func (l * LocalProvider ) GetURL (ctx context.Context , key string ) (string , error ) {
82139 if l .baseURL != "" {
83- return fmt .Sprintf ("%s/%s" , l .baseURL , key ), nil
140+ // Sanitize key for URL to prevent injection
141+ cleanKey := filepath .Clean (key )
142+ return fmt .Sprintf ("%s/%s" , l .baseURL , cleanKey ), nil
143+ }
144+
145+ filePath , err := l .securePath (key )
146+ if err != nil {
147+ return "" , err
84148 }
85- return fmt .Sprintf ("file://%s" , filepath . Join ( l . basePath , key ) ), nil
149+ return fmt .Sprintf ("file://%s" , filePath ), nil
86150}
87151
88152func (l * LocalProvider ) GetSignedURL (ctx context.Context , key string , expiration int64 ) (string , error ) {
0 commit comments