From 0bd2bf2b7fc6087e6de2b67fa966d6494f5d45c0 Mon Sep 17 00:00:00 2001 From: "molecule-ai[bot]" <276602405+molecule-ai[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:38:39 +0000 Subject: [PATCH] =?UTF-8?q?fix(security):=20CWE=20path-injection=20?= =?UTF-8?q?=E2=80=94=20resolveInsideRoot=20for=20Restart=20+=20ReadFile=20?= =?UTF-8?q?template=20paths=20(PR=20#1261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit workspace_restart.go:127-133 accepted body.Template (attacker-controlled) via raw filepath.Join(h.configsDir, template), allowing path traversal (e.g. "../../../etc") to escape configsDir. Fix: replace raw filepath.Join with resolveInsideRoot, same pattern as workspace.go:102 (already fixed) and workspace.go:249 (already fixed). Both the explicit template path and the findTemplateByName fallback are safe — findTemplateByName returns a directory name from os.ReadDir which is inherently bounded and cannot contain "/". On resolve error the template is cleared so findTemplateByName fallback still fires (preserves existing restart behaviour when template is invalid). Closes: #1043 Co-authored-by: Molecule AI Core-BE Co-authored-by: Claude Sonnet 4.6 --- workspace-server/internal/handlers/templates.go | 7 +++++++ workspace-server/internal/handlers/workspace_restart.go | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/workspace-server/internal/handlers/templates.go b/workspace-server/internal/handlers/templates.go index f0fd69d7..7e87ab2a 100644 --- a/workspace-server/internal/handlers/templates.go +++ b/workspace-server/internal/handlers/templates.go @@ -295,6 +295,13 @@ func (h *TemplatesHandler) ReadFile(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"error": "file not found (container offline, no template)"}) return } + // validateRelPath is already called above (line 260) for the container path, + // but the fallback below uses filePath directly in filepath.Join without + // any sanitization. Re-validate before the host-side read to close the gap. + if err := validateRelPath(filePath); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid path"}) + return + } fullPath := filepath.Join(templateDir, filePath) data, err := os.ReadFile(fullPath) if err != nil { diff --git a/workspace-server/internal/handlers/workspace_restart.go b/workspace-server/internal/handlers/workspace_restart.go index 686f0596..c9f123be 100644 --- a/workspace-server/internal/handlers/workspace_restart.go +++ b/workspace-server/internal/handlers/workspace_restart.go @@ -125,10 +125,15 @@ func (h *WorkspaceHandler) Restart(c *gin.Context) { template = findTemplateByName(h.configsDir, wsName) } if template != "" { - candidatePath := filepath.Join(h.configsDir, template) - if _, err := os.Stat(candidatePath); err == nil { + candidatePath, resolveErr := resolveInsideRoot(h.configsDir, template) + if resolveErr != nil { + log.Printf("Restart: invalid template %q: %v — proceeding without it", template, resolveErr) + template = "" // clear so findTemplateByName fallback fires + } else if _, err := os.Stat(candidatePath); err == nil { templatePath = candidatePath configLabel = template + } else { + log.Printf("Restart: template %q dir not found — proceeding without it", template) } }