Fix bugs found in full security and stability audit.
Harden artifact paths and fusion uploads, repair pool reconnect and login ID tracking, fix agent/fusion/frontend regressions, and refresh PROBLEMS.md with the full findings list.
This commit is contained in:
@@ -176,27 +176,31 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("prep_exe")
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion requires prep_exe file upload"})
|
||||
return
|
||||
if req.FusionEnabled {
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion requires prep_exe file upload"})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
if req.FusionMediaBaseName == "" && header.Filename != "" {
|
||||
req.FusionMediaBaseName = header.Filename
|
||||
}
|
||||
if req.FusionOutputName == "" && header.Filename != "" {
|
||||
req.FusionOutputName = header.Filename
|
||||
}
|
||||
if req.FusionPayloadKind == "" {
|
||||
req.FusionPayloadKind = detectFusionPayloadKind(header.Filename)
|
||||
}
|
||||
saved, remove, err := h.saveUploadedFusionPayload(file, header)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
prepPath = saved
|
||||
cleanupPrep = remove
|
||||
} else if err == nil {
|
||||
file.Close()
|
||||
}
|
||||
defer file.Close()
|
||||
if req.FusionMediaBaseName == "" && header.Filename != "" {
|
||||
req.FusionMediaBaseName = header.Filename
|
||||
}
|
||||
if req.FusionEnabled && req.FusionOutputName == "" && header.Filename != "" {
|
||||
req.FusionOutputName = header.Filename
|
||||
}
|
||||
if req.FusionPayloadKind == "" {
|
||||
req.FusionPayloadKind = detectFusionPayloadKind(header.Filename)
|
||||
}
|
||||
saved, remove, err := h.saveUploadedFusionPayload(file, header)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
prepPath = saved
|
||||
cleanupPrep = remove
|
||||
} else {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Invalid request body"})
|
||||
@@ -327,19 +331,28 @@ func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *Handler) DownloadBuildArtifact(w http.ResponseWriter, r *http.Request) {
|
||||
buildID := chi.URLParam(r, "id")
|
||||
if _, err := h.db.GetBuild(buildID); err != nil {
|
||||
http.Error(w, "Build not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
name := sanitizeFileName(chi.URLParam(r, "name"))
|
||||
if name == "" {
|
||||
if name == "" || strings.Contains(name, "..") {
|
||||
http.Error(w, "Invalid artifact name", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
buildDir := filepath.Join(h.dataDir, "builds", buildID)
|
||||
path := filepath.Join(buildDir, name)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
// Paired media may live in fusion export dir — try deliverables folder from query
|
||||
if exportDir := strings.TrimSpace(r.URL.Query().Get("export_dir")); exportDir != "" {
|
||||
path = filepath.Join(exportDir, name)
|
||||
path, err := safePathUnderRoot(buildDir, name)
|
||||
if err != nil {
|
||||
if title := strings.TrimSpace(r.URL.Query().Get("export_dir")); title != "" {
|
||||
title = sanitizeFileName(filepath.Base(title))
|
||||
deliverablesRoot := filepath.Join(h.projectRoot, FusionDeliverablesDir)
|
||||
path, err = safePathUnderRoot(filepath.Join(deliverablesRoot, title), name)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "Artifact not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
http.Error(w, "Artifact not found", http.StatusNotFound)
|
||||
return
|
||||
@@ -570,6 +583,7 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
|
||||
}
|
||||
if err := h.db.InsertBuild(buildRecord); err != nil {
|
||||
log.Printf("Failed to record build: %v", err)
|
||||
return BuildResponse{Success: false, Error: "Failed to record build in database"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
resp := BuildResponse{
|
||||
@@ -785,6 +799,9 @@ func (h *Handler) saveUploadedFusionPayload(file multipart.File, header *multipa
|
||||
if header.Size > FusionMaxUploadBytes {
|
||||
return "", nil, fmt.Errorf("fusion upload exceeds %s limit", formatBytes(FusionMaxUploadBytes))
|
||||
}
|
||||
if header.Size < 0 {
|
||||
return "", nil, fmt.Errorf("fusion upload size unknown — retry with a smaller file")
|
||||
}
|
||||
baseName := filepath.Base(header.Filename)
|
||||
if baseName == "" || baseName == "." {
|
||||
return "", nil, fmt.Errorf("fusion upload filename is invalid")
|
||||
@@ -807,7 +824,7 @@ func (h *Handler) saveUploadedFusionPayload(file multipart.File, header *multipa
|
||||
os.RemoveAll(dir)
|
||||
return "", nil, err
|
||||
}
|
||||
written, err := io.Copy(out, file)
|
||||
written, err := io.Copy(out, io.LimitReader(file, FusionMaxUploadBytes+1))
|
||||
out.Close()
|
||||
if err != nil {
|
||||
os.RemoveAll(dir)
|
||||
@@ -1049,6 +1066,30 @@ func sanitizeFileName(name string) string {
|
||||
return replacer.Replace(name)
|
||||
}
|
||||
|
||||
// safePathUnderRoot resolves name under root and rejects traversal escapes.
|
||||
func safePathUnderRoot(root, name string) (string, error) {
|
||||
if name == "" || strings.Contains(name, "..") {
|
||||
return "", fmt.Errorf("invalid path")
|
||||
}
|
||||
cleanName := filepath.Clean(name)
|
||||
if filepath.IsAbs(cleanName) {
|
||||
return "", fmt.Errorf("invalid path")
|
||||
}
|
||||
absRoot, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
full := filepath.Join(absRoot, cleanName)
|
||||
absFull, err := filepath.Abs(full)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if absFull != absRoot && !strings.HasPrefix(absFull, absRoot+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("path escapes root")
|
||||
}
|
||||
return absFull, nil
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
|
||||
Reference in New Issue
Block a user