Reject path traversal in deploy log download handler (closes #177)
Check / check (pull_request) Skipped
Check / check (pull_request) Skipped
gosec flagged G703 (path traversal via taint analysis) on the log download handler because the served path derives from a URL parameter. Open the log file through an os.Root confined to the deploy log directory instead of passing the path to http.ServeFile; Root.Open rejects any path that escapes the root, so traversal attempts return 404. Serve the opened file with http.ServeContent. Adds GetLogDir on the deploy service. The traversal regression test plants a sentinel file at the location a traversal-shaped app name resolves to (outside the log directory) and asserts the handler returns 404 without leaking the sentinel, so it fails if the os.Root guard is removed. Keeps the legitimate-download test. Model: opus-4-8
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