forked from molecule-ai/molecule-core
fix(security): #226 — gate POST /workspaces template/runtime against traversal
Closes #226 MEDIUM. WorkspaceHandler.Create joined payload.Template directly into filepath.Join(configsDir, template) without validating it stayed inside configsDir. An attacker posting Template="../../etc" would have the provisioner walk and mount arbitrary host directories into the workspace container. Same fix as #103 (POST /org/import): use the existing resolveInsideRoot helper to reject absolute paths and any ".." that escapes the root. Applied at both call sites in workspace.go: 1. Synchronous runtime detection before DB insert — 400 on bad input 2. Async provisioning goroutine — early return, logs the rejection (belt-and-suspenders; the create path already blocks) No test added inline because the existing resolveInsideRoot suite (org_path_test.go) already covers absolute / traversal / prefix-sibling / empty-path / deep-subpath cases. A duplicate test for the workspace handler wouldn't add signal. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
07cd0a2dfa
commit
3d561b24ef
@ -53,7 +53,15 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
||||
// Detect runtime from template config.yaml if not specified in request.
|
||||
// Must happen before DB insert so the correct runtime is persisted.
|
||||
if payload.Runtime == "" && payload.Template != "" {
|
||||
candidatePath := filepath.Join(h.configsDir, payload.Template)
|
||||
// #226: payload.Template is attacker-controllable. resolveInsideRoot
|
||||
// rejects absolute paths and any ".." that escapes configsDir so the
|
||||
// provisioner can't be pointed at host directories.
|
||||
candidatePath, resolveErr := resolveInsideRoot(h.configsDir, payload.Template)
|
||||
if resolveErr != nil {
|
||||
log.Printf("Create: invalid template path %q: %v", payload.Template, resolveErr)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid template"})
|
||||
return
|
||||
}
|
||||
cfgData, readErr := os.ReadFile(filepath.Join(candidatePath, "config.yaml"))
|
||||
if readErr != nil {
|
||||
log.Printf("Create: could not read config.yaml for template %q: %v", payload.Template, readErr)
|
||||
@ -149,7 +157,16 @@ func (h *WorkspaceHandler) Create(c *gin.Context) {
|
||||
var templatePath string
|
||||
var configFiles map[string][]byte
|
||||
if payload.Template != "" {
|
||||
candidatePath := filepath.Join(h.configsDir, payload.Template)
|
||||
// #226: re-validate the template path at provision time. Even
|
||||
// though the create-path validates above, provision runs in a
|
||||
// goroutine with a fresh payload copy — duplicate the guard so
|
||||
// a future code path that reaches provisionTenant with an
|
||||
// unchecked payload can't regress the fix.
|
||||
candidatePath, resolveErr := resolveInsideRoot(h.configsDir, payload.Template)
|
||||
if resolveErr != nil {
|
||||
log.Printf("Create provision: rejecting template %q: %v", payload.Template, resolveErr)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(candidatePath); err == nil {
|
||||
templatePath = candidatePath
|
||||
} else {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user