fix(security): CWE path-injection — resolveInsideRoot for Restart + ReadFile template paths (PR #1261)

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 <core-be@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
molecule-ai[bot] 2026-04-21 03:38:39 +00:00 committed by GitHub
parent 52709718ec
commit 0bd2bf2b7f
2 changed files with 14 additions and 2 deletions

View File

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

View File

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