Simple S3-like server for development purposes, written in Go
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix path traversal

+43 -17
+1 -1
go.mod
··· 1 1 module tangled.org/juanlu.space/gos3dir 2 2 3 - go 1.24.4 3 + go 1.25.5
+42 -16
gos3dir.go
··· 10 10 "path/filepath" 11 11 ) 12 12 13 + const rootDir = "." // TODO: Turn into a parameter 14 + 13 15 func addServerHeaders(serverName string) func(http.Handler) http.Handler { 14 16 return func(next http.Handler) http.Handler { 15 17 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ··· 35 37 } 36 38 37 39 func mb(w http.ResponseWriter, r *http.Request) { 38 - bucketName := r.PathValue("bucket") 39 - if err := os.Mkdir(bucketName, 0775); err != nil { 40 + root, err := os.OpenRoot(rootDir) 41 + if err != nil { 42 + http.Error(w, err.Error(), http.StatusInternalServerError) 43 + } 44 + defer root.Close() 45 + 46 + path := filepath.Join(r.PathValue("bucket"), r.PathValue("key")) 47 + 48 + bucketName := filepath.SplitList(path)[0] 49 + if err := root.Mkdir(bucketName, 0775); err != nil { 40 50 if errors.Is(err, fs.ErrExist) { 41 51 http.Error(w, "Your previous request to create the named bucket succeeded and you already own it.", http.StatusConflict) 42 52 } else { ··· 49 59 } 50 60 51 61 func rb(w http.ResponseWriter, r *http.Request) { 52 - bucketName := r.PathValue("bucket") 53 - files, err := os.ReadDir(bucketName) 62 + root, err := os.OpenRoot(rootDir) 63 + if err != nil { 64 + http.Error(w, err.Error(), http.StatusInternalServerError) 65 + } 66 + defer root.Close() 67 + 68 + path := filepath.Join(r.PathValue("bucket"), r.PathValue("key")) 69 + 70 + bucketName := filepath.SplitList(path)[0] 71 + files, err := fs.ReadDir(root.FS(), bucketName) 54 72 if err != nil { 55 73 if errors.Is(err, fs.ErrNotExist) { 56 74 http.Error(w, "The specified bucket does not exist", http.StatusConflict) ··· 64 82 return 65 83 } 66 84 67 - if err := os.Remove(bucketName); err != nil { 85 + if err := root.Remove(bucketName); err != nil { 68 86 http.Error(w, err.Error(), http.StatusInternalServerError) 69 87 return 70 88 } ··· 73 91 } 74 92 75 93 func cp(w http.ResponseWriter, r *http.Request) { 76 - bucketName := r.PathValue("bucket") 77 - key := r.PathValue("key") 94 + root, err := os.OpenRoot(rootDir) 95 + if err != nil { 96 + http.Error(w, err.Error(), http.StatusInternalServerError) 97 + } 98 + defer root.Close() 99 + 100 + path := filepath.Join(r.PathValue("bucket"), r.PathValue("key")) 78 101 79 102 // Prevents `cp` from accidentally creating a new bucket if it doesn't exist 80 - if _, err := os.Stat(bucketName); err != nil { 103 + bucketName := filepath.SplitList(path)[0] 104 + if _, err := root.Stat(bucketName); err != nil { 81 105 if errors.Is(err, fs.ErrNotExist) { 82 106 http.Error(w, "The specified bucket does not exist", http.StatusNotFound) 83 107 } else { ··· 86 110 return 87 111 } 88 112 89 - path := filepath.Join(bucketName, key) 90 - if err := os.MkdirAll(filepath.Dir(path), 0775); err != nil { 113 + // Safely create nested directories if needed 114 + if err := root.MkdirAll(filepath.Dir(path), 0775); err != nil { 91 115 http.Error(w, err.Error(), http.StatusInternalServerError) 92 116 return 93 117 } 94 118 95 - file, err := os.Create(path) 119 + file, err := root.Create(path) 96 120 if err != nil { 97 121 http.Error(w, err.Error(), http.StatusInternalServerError) 98 122 return ··· 109 133 } 110 134 111 135 func rm(w http.ResponseWriter, r *http.Request) { 112 - bucketName := r.PathValue("bucket") 113 - key := r.PathValue("key") 136 + root, err := os.OpenRoot(rootDir) 137 + if err != nil { 138 + http.Error(w, err.Error(), http.StatusInternalServerError) 139 + } 140 + defer root.Close() 114 141 115 142 // TODO: Dangling empty directory 116 - path := filepath.Join(bucketName, key) 117 - if err := os.Remove(path); err != nil { 143 + path := filepath.Join(r.PathValue("bucket"), r.PathValue("key")) 144 + if err := root.Remove(path); err != nil { 118 145 http.Error(w, err.Error(), http.StatusInternalServerError) 119 146 return 120 147 } ··· 123 150 } 124 151 125 152 func main() { 126 - // TODO: Prevent path traversal 127 153 // TODO: (Claude) "S3 returns ETags for uploaded objects. Consider adding MD5 hashing" 128 154 http.HandleFunc("GET /{bucket}", ls) 129 155 http.HandleFunc("PUT /{bucket}", mb)