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:
drjones
2026-05-29 09:57:22 -07:00
parent e11fb30350
commit f9e26bb1a6
13 changed files with 281 additions and 82 deletions

View File

@@ -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)