diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index ad9fff5369..62e0807c48 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -101,6 +101,7 @@ func (s *Server) downloadZIP(body io.Reader, output string) error { } defer reader.Close() trimDir := zipIsInSingleDirectory(reader.File) + cleanOutput := filepath.Clean(output) + string(os.PathSeparator) for _, file := range reader.File { if file.FileInfo().IsDir() { continue @@ -118,6 +119,10 @@ func (s *Server) downloadZIP(body io.Reader, output string) error { return err } savePath := filepath.Join(saveDirectory, pathElements[len(pathElements)-1]) + cleanSavePath := filepath.Clean(savePath) + if !strings.HasPrefix(cleanSavePath, cleanOutput) { + return os.ErrPermission + } err = downloadZIPEntry(s.ctx, file, savePath) if err != nil { return err diff --git a/experimental/clashapi/server_resources_test.go b/experimental/clashapi/server_resources_test.go new file mode 100644 index 0000000000..c239039541 --- /dev/null +++ b/experimental/clashapi/server_resources_test.go @@ -0,0 +1,46 @@ +package clashapi + +import ( + "archive/zip" + "bytes" + "context" + "os" + "path/filepath" + "testing" +) + +func TestDownloadZIPZipSlip(t *testing.T) { + tempDir := t.TempDir() + + var buf bytes.Buffer + + zw := zip.NewWriter(&buf) + + _, err := zw.Create("../../../pwned.txt") + if err != nil { + t.Fatal(err) + } + + err = zw.Close() + if err != nil { + t.Fatal(err) + } + + server := &Server{ + ctx: context.Background(), + } + + err = server.downloadZIP(bytes.NewReader(buf.Bytes()), tempDir) + + if err == nil { + t.Fatal("expected zip slip error") + } + + outsidePath := filepath.Join(tempDir, "..", "..", "..", "pwned.txt") + + _, err = os.Stat(outsidePath) + + if !os.IsNotExist(err) { + t.Fatal("zip slip created file outside target directory") + } +}