Skip to content

Commit 1328690

Browse files
committed
fix(fs): block path traversal in handlers
1 parent 2b79a39 commit 1328690

File tree

6 files changed

+157
-10
lines changed

6 files changed

+157
-10
lines changed

internal/errs/operate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ import "errors"
44

55
var (
66
PermissionDenied = errors.New("permission denied")
7+
InvalidName = errors.New("invalid file name")
78
)

pkg/utils/path.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,38 @@ func JoinBasePath(basePath, reqPath string) (string, error) {
101101
func GetFullPath(mountPath, path string) string {
102102
return stdpath.Join(GetActualMountPath(mountPath), path)
103103
}
104+
105+
// ValidateNameComponent validates a single path component.
106+
// It rejects empty names, dot segments, separators, ".." sequences, and NUL bytes.
107+
func ValidateNameComponent(name string) error {
108+
if name == "" {
109+
return errs.InvalidName
110+
}
111+
if name == "." || name == ".." {
112+
return errs.InvalidName
113+
}
114+
if strings.Contains(name, "/") || strings.Contains(name, "\\") {
115+
return errs.InvalidName
116+
}
117+
if strings.Contains(name, "..") {
118+
return errs.InvalidName
119+
}
120+
if strings.ContainsRune(name, 0) {
121+
return errs.InvalidName
122+
}
123+
return nil
124+
}
125+
126+
// JoinUnderBase safely joins baseDir with a single name component and ensures the
127+
// result stays under baseDir after normalization.
128+
func JoinUnderBase(baseDir, name string) (string, error) {
129+
if err := ValidateNameComponent(name); err != nil {
130+
return "", err
131+
}
132+
base := FixAndCleanPath(baseDir)
133+
joined := FixAndCleanPath(stdpath.Join(base, name))
134+
if !IsSubPath(base, joined) {
135+
return "", errs.InvalidName
136+
}
137+
return joined, nil
138+
}

pkg/utils/path_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,49 @@ func TestFixAndCleanPath(t *testing.T) {
2020
}
2121
}
2222
}
23+
24+
func TestValidateNameComponent(t *testing.T) {
25+
validNames := []string{
26+
"file.txt",
27+
"abc",
28+
"file_name-1",
29+
}
30+
for _, name := range validNames {
31+
if err := ValidateNameComponent(name); err != nil {
32+
t.Fatalf("expected valid name %q, got error: %v", name, err)
33+
}
34+
}
35+
36+
invalidNames := []string{
37+
"",
38+
".",
39+
"..",
40+
"a/b",
41+
`a\b`,
42+
"a..b",
43+
string([]byte{'a', 0, 'b'}),
44+
}
45+
for _, name := range invalidNames {
46+
if err := ValidateNameComponent(name); err == nil {
47+
t.Fatalf("expected invalid name %q to be rejected", name)
48+
}
49+
}
50+
}
51+
52+
func TestJoinUnderBase(t *testing.T) {
53+
base := "/lanzou-y/shared/test1"
54+
out, err := JoinUnderBase(base, "file.txt")
55+
if err != nil {
56+
t.Fatalf("expected join success, got error: %v", err)
57+
}
58+
if out != "/lanzou-y/shared/test1/file.txt" {
59+
t.Fatalf("unexpected join result: %s", out)
60+
}
61+
62+
if _, err := JoinUnderBase(base, "../admin/screts.txt"); err == nil {
63+
t.Fatalf("expected traversal to be rejected")
64+
}
65+
if _, err := JoinUnderBase(base, "sub/child"); err == nil {
66+
t.Fatalf("expected nested path to be rejected")
67+
}
68+
}

server/handles/archive.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,16 @@ func FsArchiveDecompress(c *gin.Context) {
254254
return
255255
}
256256
user := c.MustGet("user").(*model.User)
257+
srcDir, err := user.JoinPath(req.SrcDir)
258+
if err != nil {
259+
common.ErrorResp(c, err, 403)
260+
return
261+
}
257262
srcPaths := make([]string, 0, len(req.Name))
258263
for _, name := range req.Name {
259-
srcPath, err := user.JoinPath(stdpath.Join(req.SrcDir, name))
264+
srcPath, err := utils.JoinUnderBase(srcDir, name)
260265
if err != nil {
261-
common.ErrorResp(c, err, 403)
266+
common.ErrorResp(c, err, 400)
262267
return
263268
}
264269
if !common.CheckPathLimitWithRoles(user, srcPath) {

server/handles/fsbatch.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/alist-org/alist/v3/internal/model"
1111
"github.com/alist-org/alist/v3/internal/op"
1212
"github.com/alist-org/alist/v3/pkg/generic"
13+
"github.com/alist-org/alist/v3/pkg/utils"
1314
"github.com/alist-org/alist/v3/server/common"
1415
"github.com/gin-gonic/gin"
1516
"github.com/pkg/errors"
@@ -185,7 +186,15 @@ func FsBatchRename(c *gin.Context) {
185186
if renameObject.SrcName == "" || renameObject.NewName == "" {
186187
continue
187188
}
188-
filePath := fmt.Sprintf("%s/%s", reqPath, renameObject.SrcName)
189+
if err := utils.ValidateNameComponent(renameObject.NewName); err != nil {
190+
common.ErrorResp(c, err, 400)
191+
return
192+
}
193+
filePath, err := utils.JoinUnderBase(reqPath, renameObject.SrcName)
194+
if err != nil {
195+
common.ErrorResp(c, err, 400)
196+
return
197+
}
189198
if err := fs.Rename(c, filePath, renameObject.NewName); err != nil {
190199
common.ErrorResp(c, err, 500)
191200
return
@@ -247,8 +256,16 @@ func FsRegexRename(c *gin.Context) {
247256
for _, file := range files {
248257

249258
if srcRegexp.MatchString(file.GetName()) {
250-
filePath := fmt.Sprintf("%s/%s", reqPath, file.GetName())
259+
filePath, err := utils.JoinUnderBase(reqPath, file.GetName())
260+
if err != nil {
261+
common.ErrorResp(c, err, 500)
262+
return
263+
}
251264
newFileName := srcRegexp.ReplaceAllString(file.GetName(), req.NewNameRegex)
265+
if err := utils.ValidateNameComponent(newFileName); err != nil {
266+
common.ErrorResp(c, err, 400)
267+
return
268+
}
252269
if err := fs.Rename(c, filePath, newFileName); err != nil {
253270
common.ErrorResp(c, err, 500)
254271
return

server/handles/fsmanage.go

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,29 @@ func FsMove(c *gin.Context) {
103103
}
104104
if !req.Overwrite {
105105
for _, name := range req.Names {
106-
if res, _ := fs.Get(c, stdpath.Join(dstDir, name), &fs.GetArgs{NoLog: true}); res != nil {
106+
dstPath, err := utils.JoinUnderBase(dstDir, name)
107+
if err != nil {
108+
common.ErrorResp(c, err, 400)
109+
return
110+
}
111+
if res, _ := fs.Get(c, dstPath, &fs.GetArgs{NoLog: true}); res != nil {
107112
common.ErrorStrResp(c, fmt.Sprintf("file [%s] exists", name), 403)
108113
return
109114
}
110115
}
111116
}
112117
for i, name := range req.Names {
113-
err := fs.Move(c, stdpath.Join(srcDir, name), dstDir, len(req.Names) > i+1)
118+
srcPath, err := utils.JoinUnderBase(srcDir, name)
119+
if err != nil {
120+
common.ErrorResp(c, err, 400)
121+
return
122+
}
123+
_, err = utils.JoinUnderBase(dstDir, name)
124+
if err != nil {
125+
common.ErrorResp(c, err, 400)
126+
return
127+
}
128+
err = fs.Move(c, srcPath, dstDir, len(req.Names) > i+1)
114129
if err != nil {
115130
common.ErrorResp(c, err, 500)
116131
return
@@ -155,15 +170,30 @@ func FsCopy(c *gin.Context) {
155170
}
156171
if !req.Overwrite {
157172
for _, name := range req.Names {
158-
if res, _ := fs.Get(c, stdpath.Join(dstDir, name), &fs.GetArgs{NoLog: true}); res != nil {
173+
dstPath, err := utils.JoinUnderBase(dstDir, name)
174+
if err != nil {
175+
common.ErrorResp(c, err, 400)
176+
return
177+
}
178+
if res, _ := fs.Get(c, dstPath, &fs.GetArgs{NoLog: true}); res != nil {
159179
common.ErrorStrResp(c, fmt.Sprintf("file [%s] exists", name), 403)
160180
return
161181
}
162182
}
163183
}
164184
var addedTasks []task.TaskExtensionInfo
165185
for i, name := range req.Names {
166-
t, err := fs.Copy(c, stdpath.Join(srcDir, name), dstDir, len(req.Names) > i+1)
186+
srcPath, err := utils.JoinUnderBase(srcDir, name)
187+
if err != nil {
188+
common.ErrorResp(c, err, 400)
189+
return
190+
}
191+
_, err = utils.JoinUnderBase(dstDir, name)
192+
if err != nil {
193+
common.ErrorResp(c, err, 400)
194+
return
195+
}
196+
t, err := fs.Copy(c, srcPath, dstDir, len(req.Names) > i+1)
167197
if t != nil {
168198
addedTasks = append(addedTasks, t)
169199
}
@@ -204,8 +234,16 @@ func FsRename(c *gin.Context) {
204234
common.ErrorResp(c, errs.PermissionDenied, 403)
205235
return
206236
}
237+
if err := utils.ValidateNameComponent(req.Name); err != nil {
238+
common.ErrorResp(c, err, 400)
239+
return
240+
}
207241
if !req.Overwrite {
208-
dstPath := stdpath.Join(stdpath.Dir(reqPath), req.Name)
242+
dstPath, err := utils.JoinUnderBase(stdpath.Dir(reqPath), req.Name)
243+
if err != nil {
244+
common.ErrorResp(c, err, 400)
245+
return
246+
}
209247
if dstPath != reqPath {
210248
if res, _ := fs.Get(c, dstPath, &fs.GetArgs{NoLog: true}); res != nil {
211249
common.ErrorStrResp(c, fmt.Sprintf("file [%s] exists", req.Name), 403)
@@ -251,7 +289,12 @@ func FsRemove(c *gin.Context) {
251289
return
252290
}
253291
for _, name := range req.Names {
254-
err := fs.Remove(c, stdpath.Join(reqDir, name))
292+
removePath, err := utils.JoinUnderBase(reqDir, name)
293+
if err != nil {
294+
common.ErrorResp(c, err, 400)
295+
return
296+
}
297+
err = fs.Remove(c, removePath)
255298
if err != nil {
256299
common.ErrorResp(c, err, 500)
257300
return

0 commit comments

Comments
 (0)