Reject path traversal in deploy log download handler (closes #177)
The deploy-log download handler passed a request-derived path to http.ServeFile, which gosec flags as G703 (path traversal via taint). The handler now opens the log through an os.Root confined to the deploy log directory, so any escaping path is rejected at runtime (404) and the file is streamed with http.ServeContent. A regression test plants a sentinel outside the log dir and asserts the traversal is refused and its contents never served; removing the guard makes that test fail. No //nolint used. Model: opus-4-8
This commit was merged in pull request #194.
This commit is contained in:
@@ -611,7 +611,13 @@ func (h *Handlers) HandleDeploymentLogDownload() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the log file path from deploy service
|
||||
// The log path is derived from request data (the app is looked
|
||||
// up by a URL parameter), so open it through an os.Root confined
|
||||
// to the deploy log directory. Root.Open rejects any path that
|
||||
// escapes the root, so a traversal attempt fails rather than
|
||||
// serving an arbitrary file.
|
||||
logDir := h.deploy.GetLogDir()
|
||||
|
||||
logPath := h.deploy.GetLogFilePath(application, deployment)
|
||||
if logPath == "" {
|
||||
http.NotFound(writer, request)
|
||||
@@ -619,28 +625,43 @@ func (h *Handlers) HandleDeploymentLogDownload() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if file exists — logPath is constructed internally, not from user input
|
||||
_, err := os.Stat(logPath) // #nosec G703 -- internal path, not user input
|
||||
if os.IsNotExist(err) {
|
||||
relPath, relErr := filepath.Rel(logDir, logPath)
|
||||
if relErr != nil {
|
||||
http.NotFound(writer, request)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
h.log.Error("failed to stat log file", "error", err, "path", logPath)
|
||||
root, rootErr := os.OpenRoot(logDir)
|
||||
if rootErr != nil {
|
||||
http.NotFound(writer, request)
|
||||
|
||||
return
|
||||
}
|
||||
defer func() { _ = root.Close() }()
|
||||
|
||||
file, openErr := root.Open(relPath)
|
||||
if openErr != nil {
|
||||
http.NotFound(writer, request)
|
||||
|
||||
return
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
info, statErr := file.Stat()
|
||||
if statErr != nil {
|
||||
h.log.Error("failed to stat log file", "error", statErr, "path", logPath)
|
||||
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Extract filename for Content-Disposition header
|
||||
filename := filepath.Base(logPath)
|
||||
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
||||
|
||||
http.ServeFile(writer, request, logPath) // #nosec G703 -- internal path
|
||||
http.ServeContent(writer, request, filename, info.ModTime(), file)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user