Bläddra i källkod

Proper fix for #305

Toby Chui 1 vecka sedan
förälder
incheckning
5f73a245cc
3 ändrade filer med 82 tillägg och 11 borttagningar
  1. 37 0
      src/mod/filesystem/arozfs/arozfs.go
  2. 39 0
      src/mod/filesystem/arozfs/arozfs_test.go
  3. 6 11
      src/mod/share/share.go

+ 37 - 0
src/mod/filesystem/arozfs/arozfs.go

@@ -10,6 +10,7 @@ import (
 	"errors"
 	"io"
 	"io/fs"
+	"net/url"
 	"path"
 	"path/filepath"
 	"regexp"
@@ -42,6 +43,16 @@ type ShortcutData struct {
 	Name string //The name of the shortcut
 	Path string //The path of shortcut
 	Icon string //The icon of shortcut
+
+	//Optional launch options, stored as key=value lines after the first four.
+	//Only honoured for url shortcuts; module shortcuts take them from init.agi
+	WindowTitle  string //Custom floatWindow title, empty = shortcut name
+	OpenIn       string //"float" (default) or "tab"
+	WindowWidth  int    //Initial floatWindow width, 0 = default
+	WindowHeight int    //Initial floatWindow height, 0 = default
+
+	//Unknown key=value lines, kept so a rewrite does not drop them
+	Extra map[string]string `json:"-"`
 }
 
 var (
@@ -70,6 +81,7 @@ var (
 	//Operation errors
 	ErrOperationNotSupported = errors.New("FS_OPR_NOT_SUPPORTED")
 	ErrNullOperation         = errors.New("FS_NULL_OPR")
+	ErrPathEscapesRoot       = errors.New("FS_PATH_ESCAPES_ROOT")
 )
 
 // Generate a File Manager redirection error message
@@ -196,3 +208,28 @@ func Base(filename string) string {
 		return c[len(c)-1]
 	}
 }
+
+// ResolvePathWithinRoot decodes a relative path, normalizes separators, and
+// ensures the final path remains under rootPath.
+func ResolvePathWithinRoot(rootPath string, relativePath string) (string, error) {
+	decodedRelPath, err := url.PathUnescape(relativePath)
+	if err != nil {
+		return "", err
+	}
+
+	cleanRoot := filepath.Clean(rootPath)
+	normalizedRelPath := strings.ReplaceAll(decodedRelPath, "\\", "/")
+	targetPath := filepath.Join(cleanRoot, filepath.FromSlash(normalizedRelPath))
+
+	relToRoot, err := filepath.Rel(cleanRoot, targetPath)
+	if err != nil {
+		return "", err
+	}
+
+	cleanRelToRoot := filepath.ToSlash(relToRoot)
+	if cleanRelToRoot == ".." || strings.HasPrefix(cleanRelToRoot, "../") {
+		return "", ErrPathEscapesRoot
+	}
+
+	return targetPath, nil
+}

+ 39 - 0
src/mod/filesystem/arozfs/arozfs_test.go

@@ -1,6 +1,8 @@
 package arozfs
 
 import (
+	"errors"
+	"path/filepath"
 	"strings"
 	"testing"
 )
@@ -303,6 +305,43 @@ func TestBase_MultipleTrailingSlashes(t *testing.T) {
 	}
 }
 
+func TestResolvePathWithinRootAllowsNestedFile(t *testing.T) {
+	root := filepath.Join("root", "shared")
+	resolved, err := ResolvePathWithinRoot(root, "docs/report.txt")
+	if err != nil {
+		t.Fatalf("ResolvePathWithinRoot returned unexpected error: %v", err)
+	}
+
+	expected := filepath.Join(root, "docs", "report.txt")
+	if resolved != expected {
+		t.Fatalf("ResolvePathWithinRoot = %q, want %q", resolved, expected)
+	}
+}
+
+func TestResolvePathWithinRootRejectsTraversal(t *testing.T) {
+	root := filepath.Join("root", "shared")
+	inputs := []string{
+		"../secret.txt",
+		"..%2fsecret.txt",
+		"..\\secret.txt",
+		"nested/../../secret.txt",
+		"..%5csecret.txt",
+	}
+
+	for _, input := range inputs {
+		if _, err := ResolvePathWithinRoot(root, input); !errors.Is(err, ErrPathEscapesRoot) {
+			t.Fatalf("ResolvePathWithinRoot(%q) error = %v, want %v", input, err, ErrPathEscapesRoot)
+		}
+	}
+}
+
+func TestResolvePathWithinRootRejectsSiblingPrefixBypass(t *testing.T) {
+	root := filepath.Join("root", "share")
+	if _, err := ResolvePathWithinRoot(root, "../share-other/file.txt"); !errors.Is(err, ErrPathEscapesRoot) {
+		t.Fatalf("ResolvePathWithinRoot error = %v, want %v", err, ErrPathEscapesRoot)
+	}
+}
+
 // --- GenericVirtualPathToRealPathTranslator ---
 
 func TestGenericVirtualPathToRealPathTranslator_UserHierarchy(t *testing.T) {

+ 6 - 11
src/mod/share/share.go

@@ -570,7 +570,12 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 			if directDownload {
 				if relpath != "" {
 					//User specified a specific file within the directory. Escape the relpath
-					targetFilepath := filepath.Join(fileRuntimeAbsPath, relpath)
+					targetFilepath, err := arozfs.ResolvePathWithinRoot(fileRuntimeAbsPath, relpath)
+					if err != nil {
+						w.WriteHeader(http.StatusBadRequest)
+						w.Write([]byte("400 - Bad Request: Invalid relative path"))
+						return
+					}
 
 					//Check if file exists
 					if !targetFshAbs.FileExists(targetFilepath) {
@@ -578,16 +583,6 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 						return
 					}
 
-					//Validate the absolute path to prevent path escape
-					reqPath := filepath.ToSlash(filepath.Clean(targetFilepath))
-					rootPath, _ := targetFshAbs.VirtualPathToRealPath(shareOption.FileVirtualPath, shareOption.Owner)
-					if !strings.HasPrefix(arozfs.ToSlash(reqPath), arozfs.ToSlash(rootPath)) {
-						//Directory escape detected
-						w.WriteHeader(http.StatusBadRequest)
-						w.Write([]byte("400 - Bad Request: Invalid relative path"))
-						return
-					}
-
 					//Serve the target file
 					w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+strings.ReplaceAll(url.QueryEscape(arozfs.Base(targetFilepath)), "+", "%20"))
 					w.Header().Set("Content-Type", r.Header.Get("Content-Type"))