mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-12 22:33:54 -04:00
fix(worker): resolve staging directory symlinks
Resolve allowed directories before comparing them with resolved files. Otherwise staging rejects valid files under macOS temporary paths. Cover aliased roots, sibling paths, and symlinks escaping the root. Assisted-by: Codex:gpt-6
This commit is contained in:
1 parent
2756eaeab0
commit
4e4597dfc2
3 files changed
+40
No files matched your search
@@ -21,6 +21,10 @@ func isPathAllowed(path string, allowedDirs []string) bool {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Compare both sides after resolving aliases such as macOS /var.
|
||||
if resolvedDir, err := filepath.EvalSymlinks(absDir); err == nil {
|
||||
absDir = resolvedDir
|
||||
}
|
||||
if strings.HasPrefix(resolved, absDir+string(filepath.Separator)) || resolved == absDir {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("worker staging path containment", func() {
|
||||
DescribeTable("checks resolved directory boundaries",
|
||||
func(relative string, allowed bool) {
|
||||
root := GinkgoT().TempDir()
|
||||
models := filepath.Join(root, "models")
|
||||
sibling := filepath.Join(root, "models-other")
|
||||
Expect(os.Mkdir(models, 0o750)).To(Succeed())
|
||||
Expect(os.Mkdir(sibling, 0o750)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(models, "result.bin"), []byte("output"), 0o600)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(sibling, "private.bin"), []byte("private"), 0o600)).To(Succeed())
|
||||
Expect(os.Symlink(sibling, filepath.Join(models, "escape"))).To(Succeed())
|
||||
alias := filepath.Join(root, "alias")
|
||||
Expect(os.Symlink(root, alias)).To(Succeed())
|
||||
|
||||
Expect(isPathAllowed(filepath.Join(alias, relative), []string{filepath.Join(alias, "models")})).To(Equal(allowed))
|
||||
},
|
||||
Entry("file beneath a symlinked root", "models/result.bin", true),
|
||||
Entry("the symlinked root itself", "models", true),
|
||||
Entry("a sibling sharing the directory prefix", "models-other/private.bin", false),
|
||||
Entry("a symlink escaping the allowed root", "models/escape/private.bin", false),
|
||||
)
|
||||
})
|
||||
@@ -431,6 +431,10 @@ operator inspects on the wire has a new shape. The one difference is that a
|
||||
worker now OMITS an empty reply field where the NATS handlers always emitted it,
|
||||
which a client reading a missing field as the zero value cannot tell apart.
|
||||
|
||||
`files/stage` accepts files inside the worker's models or staging-cache
|
||||
directory, including when the directory path contains a symlink. It compares
|
||||
resolved paths and rejects existing symlinks that point outside those directories.
|
||||
|
||||
`POST /v1/control/backend/stop` is served by BOTH kinds of worker, and the
|
||||
frontend sends it the same way to either. A serve-backend worker kills the
|
||||
backend process and recycles its port; an agent worker runs no backend
|
||||
|
||||
Reference in new issue
Block a user