Skip to content

Commit 1c9470a

Browse files
vitess-bot[bot]vitess-bot
authored andcommitted
Address dir traversal in file backup storage .ListBackups(...) (#18814)
Signed-off-by: Tim Vaillancourt <tim@timvaillancourt.com>
1 parent 58344e1 commit 1c9470a

3 files changed

Lines changed: 112 additions & 11 deletions

File tree

go/fileutil/join.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
Copyright 2025 The Vitess Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package fileutil
18+
19+
import (
20+
"errors"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
)
25+
26+
var ErrInvalidJoinedPath = errors.New("invalid joined path")
27+
28+
// SafePathJoin joins file paths using a rootPath and one or many other paths,
29+
// returning a single absolute path. An error is returned if the joined path
30+
// causes a directory traversal to a path outside of the provided rootPath.
31+
func SafePathJoin(rootPath string, joinPaths ...string) (string, error) {
32+
allPaths := make([]string, 0, len(joinPaths)+1)
33+
allPaths = append(allPaths, rootPath)
34+
allPaths = append(allPaths, joinPaths...)
35+
p := filepath.Join(allPaths...)
36+
absPath, err := filepath.Abs(p)
37+
if err != nil {
38+
return p, err
39+
}
40+
absRootPath, err := filepath.Abs(rootPath)
41+
if err != nil {
42+
return absPath, err
43+
}
44+
if absPath != absRootPath && !strings.HasPrefix(absPath, absRootPath+string(os.PathSeparator)) {
45+
return absPath, ErrInvalidJoinedPath
46+
}
47+
return absPath, nil
48+
}

go/fileutil/join_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright 2025 The Vitess Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package fileutil
18+
19+
import (
20+
"path/filepath"
21+
"testing"
22+
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestSafePathJoin(t *testing.T) {
27+
rootDir := t.TempDir()
28+
29+
t.Run("success", func(t *testing.T) {
30+
path, err := SafePathJoin(rootDir, "good/path")
31+
require.NoError(t, err)
32+
require.True(t, filepath.IsAbs(path))
33+
require.Equal(t, filepath.Join(rootDir, "good/path"), path)
34+
})
35+
36+
t.Run("dir-traversal", func(t *testing.T) {
37+
_, err := SafePathJoin(rootDir, "../../..")
38+
require.ErrorIs(t, err, ErrInvalidJoinedPath)
39+
})
40+
}

go/vt/mysqlctl/filebackupstorage/file.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ import (
2727

2828
"github.com/spf13/pflag"
2929

30-
"vitess.io/vitess/go/os2"
31-
"vitess.io/vitess/go/vt/mysqlctl/errors"
32-
30+
"vitess.io/vitess/go/fileutil"
3331
"vitess.io/vitess/go/ioutil"
32+
"vitess.io/vitess/go/os2"
3433
stats "vitess.io/vitess/go/vt/mysqlctl/backupstats"
3534
"vitess.io/vitess/go/vt/mysqlctl/backupstorage"
35+
"vitess.io/vitess/go/vt/mysqlctl/errors"
3636
"vitess.io/vitess/go/vt/servenv"
3737
"vitess.io/vitess/go/vt/utils"
3838
)
@@ -127,7 +127,10 @@ func (fbh *FileBackupHandle) ReadFile(ctx context.Context, filename string) (io.
127127
if !fbh.readOnly {
128128
return nil, fmt.Errorf("ReadFile cannot be called on read-write backup")
129129
}
130-
p := path.Join(FileBackupStorageRoot, fbh.dir, fbh.name, filename)
130+
p, err := fileutil.SafePathJoin(FileBackupStorageRoot, fbh.dir, fbh.name, filename)
131+
if err != nil {
132+
return nil, err
133+
}
131134
f, err := os.Open(p)
132135
if err != nil {
133136
return nil, err
@@ -147,9 +150,13 @@ func newFileBackupStorage(params backupstorage.Params) *FileBackupStorage {
147150

148151
// ListBackups is part of the BackupStorage interface
149152
func (fbs *FileBackupStorage) ListBackups(ctx context.Context, dir string) ([]backupstorage.BackupHandle, error) {
150-
// ReadDir already sorts the results
151-
p := path.Join(FileBackupStorageRoot, dir)
152-
fi, err := os.ReadDir(p)
153+
// Check dir is not a directory traversal.
154+
path, err := fileutil.SafePathJoin(FileBackupStorageRoot, dir)
155+
if err != nil {
156+
return nil, fmt.Errorf("failed to parse backup path %q: %w", path, err)
157+
}
158+
159+
fi, err := os.ReadDir(path)
153160
if err != nil {
154161
if os.IsNotExist(err) {
155162
return nil, nil
@@ -173,14 +180,17 @@ func (fbs *FileBackupStorage) ListBackups(ctx context.Context, dir string) ([]ba
173180
// StartBackup is part of the BackupStorage interface
174181
func (fbs *FileBackupStorage) StartBackup(ctx context.Context, dir, name string) (backupstorage.BackupHandle, error) {
175182
// Make sure the directory exists.
176-
p := path.Join(FileBackupStorageRoot, dir)
177-
if err := os2.MkdirAll(p); err != nil {
183+
p, err := fileutil.SafePathJoin(FileBackupStorageRoot, dir)
184+
if err != nil {
185+
return nil, err
186+
}
187+
if err = os2.MkdirAll(p); err != nil {
178188
return nil, err
179189
}
180190

181191
// Create the subdirectory for this named backup.
182192
p = path.Join(p, name)
183-
if err := os2.Mkdir(p); err != nil {
193+
if err = os2.Mkdir(p); err != nil {
184194
return nil, err
185195
}
186196

@@ -189,7 +199,10 @@ func (fbs *FileBackupStorage) StartBackup(ctx context.Context, dir, name string)
189199

190200
// RemoveBackup is part of the BackupStorage interface
191201
func (fbs *FileBackupStorage) RemoveBackup(ctx context.Context, dir, name string) error {
192-
p := path.Join(FileBackupStorageRoot, dir, name)
202+
p, err := fileutil.SafePathJoin(FileBackupStorageRoot, dir, name)
203+
if err != nil {
204+
return err
205+
}
193206
return os.RemoveAll(p)
194207
}
195208

0 commit comments

Comments
 (0)