Skip to content

Commit 479bfb2

Browse files
[release-23.0] backupengine: disallow path traversals via backup MANIFEST on restore (#19470) (#19478)
Signed-off-by: Tim Vaillancourt <tim@timvaillancourt.com> Co-authored-by: vitess-bot[bot] <108069721+vitess-bot[bot]@users.noreply.github.com>
1 parent 0547027 commit 479bfb2

2 files changed

Lines changed: 94 additions & 2 deletions

File tree

go/vt/mysqlctl/builtinbackupengine.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/spf13/pflag"
3737
"golang.org/x/sync/errgroup"
3838

39+
"vitess.io/vitess/go/fileutil"
3940
"vitess.io/vitess/go/ioutil"
4041
"vitess.io/vitess/go/mysql"
4142
"vitess.io/vitess/go/mysql/replication"
@@ -179,7 +180,9 @@ func registerBuiltinBackupEngineFlags(fs *pflag.FlagSet) {
179180
fs.StringVar(&builtinIncrementalRestorePath, "builtinbackup-incremental-restore-path", builtinIncrementalRestorePath, "the directory where incremental restore files, namely binlog files, are extracted to. In k8s environments, this should be set to a directory that is shared between the vttablet and mysqld pods. The path should exist. When empty, the default OS temp dir is assumed.")
180181
}
181182

182-
// fullPath returns the full path of the entry, based on its type
183+
// fullPath returns the full path of the entry, based on its type.
184+
// It validates that the resolved path does not escape the base directory
185+
// via path traversal (e.g. "../../" sequences in fe.Name).
183186
func (fe *FileEntry) fullPath(cnf *Mycnf) (string, error) {
184187
// find the root to use
185188
var root string
@@ -196,7 +199,7 @@ func (fe *FileEntry) fullPath(cnf *Mycnf) (string, error) {
196199
return "", vterrors.Errorf(vtrpcpb.Code_UNKNOWN, "unknown base: %v", fe.Base)
197200
}
198201

199-
return path.Join(fe.ParentPath, root, fe.Name), nil
202+
return fileutil.SafePathJoin(path.Join(fe.ParentPath, root), fe.Name)
200203
}
201204

202205
// open attempts to open the file

go/vt/mysqlctl/builtinbackupengine_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import (
2121
"testing"
2222

2323
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
2425

26+
"vitess.io/vitess/go/fileutil"
2527
tabletmanagerdatapb "vitess.io/vitess/go/vt/proto/tabletmanagerdata"
2628
)
2729

@@ -70,6 +72,93 @@ func TestGetIncrementalFromPosGTIDSet(t *testing.T) {
7072
}
7173
}
7274

75+
func TestFileEntryFullPath(t *testing.T) {
76+
cnf := &Mycnf{
77+
DataDir: "/vt/data",
78+
InnodbDataHomeDir: "/vt/innodb-data",
79+
InnodbLogGroupHomeDir: "/vt/innodb-log",
80+
BinLogPath: "/vt/binlogs/mysql-bin",
81+
}
82+
83+
tests := []struct {
84+
name string
85+
entry FileEntry
86+
wantPath string
87+
wantError error
88+
}{
89+
{
90+
name: "valid relative path in DataDir",
91+
entry: FileEntry{Base: backupData, Name: "mydb/table1.ibd"},
92+
wantPath: "/vt/data/mydb/table1.ibd",
93+
},
94+
{
95+
name: "valid relative path in InnodbDataHomeDir",
96+
entry: FileEntry{Base: backupInnodbDataHomeDir, Name: "ibdata1"},
97+
wantPath: "/vt/innodb-data/ibdata1",
98+
},
99+
{
100+
name: "valid relative path in InnodbLogGroupHomeDir",
101+
entry: FileEntry{Base: backupInnodbLogGroupHomeDir, Name: "ib_logfile0"},
102+
wantPath: "/vt/innodb-log/ib_logfile0",
103+
},
104+
{
105+
name: "valid relative path in BinlogDir",
106+
entry: FileEntry{Base: backupBinlogDir, Name: "mysql-bin.000001"},
107+
wantPath: "/vt/binlogs/mysql-bin.000001",
108+
},
109+
{
110+
name: "valid path with ParentPath",
111+
entry: FileEntry{Base: backupData, Name: "mydb/table1.ibd", ParentPath: "/tmp/restore"},
112+
wantPath: "/tmp/restore/vt/data/mydb/table1.ibd",
113+
},
114+
{
115+
name: "path traversal escapes base directory",
116+
entry: FileEntry{Base: backupData, Name: "../../etc/passwd"},
117+
wantError: fileutil.ErrInvalidJoinedPath,
118+
},
119+
{
120+
name: "path traversal with deeper nesting",
121+
entry: FileEntry{Base: backupData, Name: "mydb/../../../etc/shadow"},
122+
wantError: fileutil.ErrInvalidJoinedPath,
123+
},
124+
{
125+
name: "path traversal to root",
126+
entry: FileEntry{Base: backupData, Name: "../../../../../etc/crontab"},
127+
wantError: fileutil.ErrInvalidJoinedPath,
128+
},
129+
{
130+
name: "path traversal escapes ParentPath",
131+
entry: FileEntry{Base: backupData, Name: "../../../../etc/passwd", ParentPath: "/tmp/restore"},
132+
wantError: fileutil.ErrInvalidJoinedPath,
133+
},
134+
{
135+
name: "relative path with dot-dot that stays within base",
136+
entry: FileEntry{Base: backupData, Name: "mydb/../mydb/table1.ibd"},
137+
wantPath: "/vt/data/mydb/table1.ibd",
138+
},
139+
}
140+
141+
// Test unknown base separately since it returns a different error type.
142+
t.Run("unknown base", func(t *testing.T) {
143+
entry := FileEntry{Base: "unknown", Name: "file"}
144+
_, err := entry.fullPath(cnf)
145+
require.Error(t, err)
146+
assert.Contains(t, err.Error(), "unknown base")
147+
})
148+
149+
for _, tt := range tests {
150+
t.Run(tt.name, func(t *testing.T) {
151+
got, err := tt.entry.fullPath(cnf)
152+
if tt.wantError != nil {
153+
require.ErrorIs(t, err, tt.wantError)
154+
} else {
155+
require.NoError(t, err)
156+
assert.Equal(t, tt.wantPath, got)
157+
}
158+
})
159+
}
160+
}
161+
73162
func TestShouldDrainForBackupBuiltIn(t *testing.T) {
74163
be := &BuiltinBackupEngine{}
75164

0 commit comments

Comments
 (0)