mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-14 06:47:28 +00:00
fix(files): allow symlinked storage roots
This commit is contained in:
parent
105ff0ed8a
commit
5474d4e503
2 changed files with 51 additions and 1 deletions
|
|
@ -103,6 +103,45 @@ func TestSymlinkEscapeBlocked(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSafeJoinAllowsSymlinkedAreaRoot(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
realRoot := filepath.Join(dir, "real-root")
|
||||||
|
if err := os.Mkdir(realRoot, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
aliasRoot := filepath.Join(dir, "alias-root")
|
||||||
|
if err := os.Symlink(realRoot, aliasRoot); err != nil {
|
||||||
|
t.Skipf("symlink unsupported: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
joined, err := safeJoin(aliasRoot, "notes.txt")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("safeJoin should allow paths under a symlinked/canonicalized root: %v", err)
|
||||||
|
}
|
||||||
|
if !within(filepath.Clean(aliasRoot), joined) {
|
||||||
|
t.Fatalf("safeJoin returned %q, want under lexical root %q", joined, aliasRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSafeJoinRejectsEscapeThroughSymlinkedAreaRoot(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
realRoot := filepath.Join(dir, "real-root")
|
||||||
|
if err := os.Mkdir(realRoot, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
aliasRoot := filepath.Join(dir, "alias-root")
|
||||||
|
if err := os.Symlink(realRoot, aliasRoot); err != nil {
|
||||||
|
t.Skipf("symlink unsupported: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink("/etc", filepath.Join(realRoot, "escape")); err != nil {
|
||||||
|
t.Skipf("symlink unsupported: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := safeJoin(aliasRoot, "escape/passwd"); err == nil {
|
||||||
|
t.Fatal("safeJoin should reject paths that escape through a symlink below the area root")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestOwnPublicWritable(t *testing.T) {
|
func TestOwnPublicWritable(t *testing.T) {
|
||||||
svc, _, u := newTestService(t)
|
svc, _, u := newTestService(t)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,17 +103,28 @@ func (s *session) resolve(p string) (resolved, error) {
|
||||||
// safeJoin joins rel onto root and verifies the result stays within root, both
|
// safeJoin joins rel onto root and verifies the result stays within root, both
|
||||||
// lexically and after resolving any symlinks that already exist on the path.
|
// lexically and after resolving any symlinks that already exist on the path.
|
||||||
func safeJoin(root, rel string) (string, error) {
|
func safeJoin(root, rel string) (string, error) {
|
||||||
|
root = filepath.Clean(root)
|
||||||
full := filepath.Join(root, filepath.FromSlash(rel))
|
full := filepath.Join(root, filepath.FromSlash(rel))
|
||||||
full = filepath.Clean(full)
|
full = filepath.Clean(full)
|
||||||
if !within(root, full) {
|
if !within(root, full) {
|
||||||
return "", errEscape
|
return "", errEscape
|
||||||
}
|
}
|
||||||
|
|
||||||
// Symlink guard: resolve the longest existing prefix and re-check. This
|
// Symlink guard: resolve the longest existing prefix and re-check. This
|
||||||
// catches a symlink (created out-of-band) that points outside the area.
|
// catches a symlink (created out-of-band) that points outside the area.
|
||||||
|
// Canonicalize the area root too; otherwise a legitimate path under a root
|
||||||
|
// reached through an OS symlink (for example /var -> /private/var on macOS, or
|
||||||
|
// a symlinked data directory) can look like it escaped once EvalSymlinks is
|
||||||
|
// applied to the probe.
|
||||||
|
resolvedRoot := root
|
||||||
|
if rr, err := filepath.EvalSymlinks(root); err == nil {
|
||||||
|
resolvedRoot = filepath.Clean(rr)
|
||||||
|
}
|
||||||
probe := full
|
probe := full
|
||||||
for {
|
for {
|
||||||
if resolvedPath, err := filepath.EvalSymlinks(probe); err == nil {
|
if resolvedPath, err := filepath.EvalSymlinks(probe); err == nil {
|
||||||
if !within(root, resolvedPath) {
|
resolvedPath = filepath.Clean(resolvedPath)
|
||||||
|
if !within(resolvedRoot, resolvedPath) {
|
||||||
return "", errEscape
|
return "", errEscape
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue