66 "net/http"
77 "os"
88 "path/filepath"
9+ "strings"
910
1011 "github.com/go-chi/chi/v5"
1112 "github.com/sarg3nt/gearbox/internal/framework/database"
@@ -111,7 +112,7 @@ func (h *Handler) APIRestoreBackup(w http.ResponseWriter, r *http.Request) {
111112 }
112113
113114 // Convert to absolute paths for comparison
114- absBackupDir , err := filepath .Abs (backupDir )
115+ absBackupDir , err := filepath .Abs (filepath . Clean ( backupDir ) )
115116 if err != nil {
116117 w .WriteHeader (http .StatusInternalServerError )
117118 json .NewEncoder (w ).Encode (map [string ]string { //#nosec G104
@@ -120,7 +121,7 @@ func (h *Handler) APIRestoreBackup(w http.ResponseWriter, r *http.Request) {
120121 return
121122 }
122123
123- absBackupPath , err := filepath .Abs (req .BackupPath )
124+ absBackupPath , err := filepath .Abs (filepath . Clean ( req .BackupPath ) )
124125 if err != nil {
125126 w .WriteHeader (http .StatusBadRequest )
126127 json .NewEncoder (w ).Encode (map [string ]string { //#nosec G104
@@ -129,17 +130,18 @@ func (h *Handler) APIRestoreBackup(w http.ResponseWriter, r *http.Request) {
129130 return
130131 }
131132
132- // Ensure the backup file is within the backup directory
133- if filepath .Dir (absBackupPath ) != absBackupDir {
133+ // Ensure the backup file is within the backup directory (HasPrefix guards
134+ // against traversal that lands in a sibling directory with a shared prefix).
135+ if ! strings .HasPrefix (absBackupPath , absBackupDir + string (filepath .Separator )) {
134136 w .WriteHeader (http .StatusBadRequest )
135137 json .NewEncoder (w ).Encode (map [string ]string { //#nosec G104
136138 "error" : "Backup file must be in the backup directory" ,
137139 })
138140 return
139141 }
140142
141- // Restore from backup
142- if err := h .db .RestoreFromBackup (req . BackupPath ); err != nil {
143+ // Restore from backup using the validated absolute path
144+ if err := h .db .RestoreFromBackup (absBackupPath ); err != nil {
143145 apperrors .WriteHTTPError (w , h .logger , apperrors .Internal ("restore backup" , err ))
144146 return
145147 }
@@ -178,7 +180,7 @@ func (h *Handler) APIDeleteBackup(w http.ResponseWriter, r *http.Request) {
178180 backupDir = "/data/backups"
179181 }
180182
181- absBackupDir , err := filepath .Abs (backupDir )
183+ absBackupDir , err := filepath .Abs (filepath . Clean ( backupDir ) )
182184 if err != nil {
183185 w .WriteHeader (http .StatusInternalServerError )
184186 json .NewEncoder (w ).Encode (map [string ]string { //#nosec G104
@@ -187,7 +189,7 @@ func (h *Handler) APIDeleteBackup(w http.ResponseWriter, r *http.Request) {
187189 return
188190 }
189191
190- absBackupPath , err := filepath .Abs (backupPath )
192+ absBackupPath , err := filepath .Abs (filepath . Clean ( backupPath ) )
191193 if err != nil {
192194 w .WriteHeader (http .StatusBadRequest )
193195 json .NewEncoder (w ).Encode (map [string ]string { //#nosec G104
@@ -196,21 +198,21 @@ func (h *Handler) APIDeleteBackup(w http.ResponseWriter, r *http.Request) {
196198 return
197199 }
198200
199- if filepath . Dir (absBackupPath ) != absBackupDir {
201+ if ! strings . HasPrefix (absBackupPath , absBackupDir + string ( filepath . Separator )) {
200202 w .WriteHeader (http .StatusBadRequest )
201203 json .NewEncoder (w ).Encode (map [string ]string { //#nosec G104
202204 "error" : "Backup file must be in the backup directory" ,
203205 })
204206 return
205207 }
206208
207- // Delete backup
208- if err := database .DeleteBackup (backupPath ); err != nil {
209+ // Delete backup using the validated absolute path
210+ if err := database .DeleteBackup (absBackupPath ); err != nil {
209211 apperrors .WriteHTTPError (w , h .logger , apperrors .Internal ("delete backup" , err ))
210212 return
211213 }
212214
213- h .logAudit (r , user .ID , "backup_deleted" , fmt .Sprintf ("Deleted database backup: %s" , filepath .Base (backupPath )))
215+ h .logAudit (r , user .ID , "backup_deleted" , fmt .Sprintf ("Deleted database backup: %s" , filepath .Base (absBackupPath )))
214216
215217 w .Header ().Set ("Content-Type" , "application/json" )
216218 json .NewEncoder (w ).Encode (map [string ]interface {}{ //#nosec G104
@@ -241,33 +243,33 @@ func (h *Handler) APIDownloadBackup(w http.ResponseWriter, r *http.Request) {
241243 backupDir = "/data/backups"
242244 }
243245
244- absBackupDir , err := filepath .Abs (backupDir )
246+ absBackupDir , err := filepath .Abs (filepath . Clean ( backupDir ) )
245247 if err != nil {
246248 http .Error (w , "Failed to resolve backup directory" , http .StatusInternalServerError )
247249 return
248250 }
249251
250- absBackupPath , err := filepath .Abs (backupPath )
252+ absBackupPath , err := filepath .Abs (filepath . Clean ( backupPath ) )
251253 if err != nil {
252254 http .Error (w , "Invalid backup path" , http .StatusBadRequest )
253255 return
254256 }
255257
256- if filepath . Dir (absBackupPath ) != absBackupDir {
258+ if ! strings . HasPrefix (absBackupPath , absBackupDir + string ( filepath . Separator )) {
257259 http .Error (w , "Backup file must be in the backup directory" , http .StatusBadRequest )
258260 return
259261 }
260262
261- // Check if file exists
262- if _ , err := os .Stat (backupPath ); os .IsNotExist (err ) {
263+ // Check if file exists using the validated absolute path
264+ if _ , err := os .Stat (absBackupPath ); os .IsNotExist (err ) {
263265 http .Error (w , "Backup file not found" , http .StatusNotFound )
264266 return
265267 }
266268
267- h .logAudit (r , user .ID , "backup_downloaded" , fmt .Sprintf ("Downloaded database backup: %s" , filepath .Base (backupPath )))
269+ h .logAudit (r , user .ID , "backup_downloaded" , fmt .Sprintf ("Downloaded database backup: %s" , filepath .Base (absBackupPath )))
268270
269- // Serve the file
270- w .Header ().Set ("Content-Disposition" , fmt .Sprintf (" attachment; filename=%s" , filepath .Base (backupPath )))
271+ // Serve the file; quote the filename to handle spaces and special chars safely
272+ w .Header ().Set ("Content-Disposition" , fmt .Sprintf (` attachment; filename=" %s"` , filepath .Base (absBackupPath )))
271273 w .Header ().Set ("Content-Type" , "application/octet-stream" )
272- http .ServeFile (w , r , backupPath )
274+ http .ServeFile (w , r , absBackupPath )
273275}
0 commit comments