Skip to content

Commit 9b62099

Browse files
authored
Merge pull request #830 from slackhq/slack-22_0_4-batch-02
v22.0.4 backport batch 2: backup & security hardening
2 parents 5e7acd3 + 7f5b668 commit 9b62099

17 files changed

Lines changed: 305 additions & 43 deletions

File tree

go/flags/endtoend/vtbackup.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Flags:
131131
--external-compressor string command with arguments to use when compressing a backup.
132132
--external-compressor-extension string extension to use when using an external compressor.
133133
--external-decompressor string command with arguments to use when decompressing a backup.
134+
--external-decompressor-use-manifest allows the decompressor command stored in the backup manifest to be used at restore time. Enabling this is a security risk: an attacker with write access to the backup storage could modify the manifest to execute arbitrary commands on the tablet as the Vitess user. NOT RECOMMENDED.
134135
--file_backup_storage_root string Root directory for the file backup storage.
135136
--gcs_backup_storage_bucket string Google Cloud Storage bucket to use for backups.
136137
--gcs_backup_storage_root string Root prefix for all backup-related object names.

go/flags/endtoend/vtcombo.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ Flags:
132132
--external-compressor string command with arguments to use when compressing a backup.
133133
--external-compressor-extension string extension to use when using an external compressor.
134134
--external-decompressor string command with arguments to use when decompressing a backup.
135+
--external-decompressor-use-manifest allows the decompressor command stored in the backup manifest to be used at restore time. Enabling this is a security risk: an attacker with write access to the backup storage could modify the manifest to execute arbitrary commands on the tablet as the Vitess user. NOT RECOMMENDED.
135136
--external_topo_server Should vtcombo use an external topology server instead of starting its own in-memory topology server. If true, vtcombo will use the flags defined in topo/server.go to open topo server
136137
--foreign_key_mode string This is to provide how to handle foreign key constraint in create/alter table. Valid values are: allow, disallow (default "allow")
137138
--gate_query_cache_memory int gate server query cache size in bytes, maximum amount of memory to be cached. vtgate analyzes every incoming query and generate a query plan, these plans are being cached in a lru cache. This config controls the capacity of the lru cache. (default 33554432)

go/flags/endtoend/vttablet.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ Flags:
156156
--external-compressor string command with arguments to use when compressing a backup.
157157
--external-compressor-extension string extension to use when using an external compressor.
158158
--external-decompressor string command with arguments to use when decompressing a backup.
159+
--external-decompressor-use-manifest allows the decompressor command stored in the backup manifest to be used at restore time. Enabling this is a security risk: an attacker with write access to the backup storage could modify the manifest to execute arbitrary commands on the tablet as the Vitess user. NOT RECOMMENDED.
159160
--file_backup_storage_root string Root directory for the file backup storage.
160161
--filecustomrules string file based custom rule path
161162
--filecustomrules_watch set up a watch on the target file and reload query rules when it changes

go/flags/endtoend/vttestserver.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Flags:
3838
--external-compressor string command with arguments to use when compressing a backup.
3939
--external-compressor-extension string extension to use when using an external compressor.
4040
--external-decompressor string command with arguments to use when decompressing a backup.
41+
--external-decompressor-use-manifest allows the decompressor command stored in the backup manifest to be used at restore time. Enabling this is a security risk: an attacker with write access to the backup storage could modify the manifest to execute arbitrary commands on the tablet as the Vitess user. NOT RECOMMENDED.
4142
--external_topo_global_root string the path of the global topology data in the global topology server for vtcombo process
4243
--external_topo_global_server_address string the address of the global topology server for vtcombo process
4344
--external_topo_implementation string the topology implementation to use for vtcombo process

go/test/endtoend/backup/vtctlbackup/backup_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func TestBuiltinBackupWithExternalZstdCompressionAndManifestedDecompressor(t *te
6060
CompressorEngineName: "external",
6161
ExternalCompressorCmd: "zstd",
6262
ExternalCompressorExt: ".zst",
63+
ExternalDecompressorUseManifest: true,
6364
ManifestExternalDecompressorCmd: "zstd -d",
6465
}
6566

go/test/endtoend/backup/vtctlbackup/backup_utils.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ type CompressionDetails struct {
9292
ExternalCompressorCmd string
9393
ExternalCompressorExt string
9494
ExternalDecompressorCmd string
95+
ExternalDecompressorUseManifest bool
9596
ManifestExternalDecompressorCmd string
9697
}
9798

@@ -305,6 +306,9 @@ func getCompressorArgs(cDetails *CompressionDetails) []string {
305306
if cDetails.ExternalDecompressorCmd != "" {
306307
args = append(args, fmt.Sprintf("--external-decompressor=%s", cDetails.ExternalDecompressorCmd))
307308
}
309+
if cDetails.ExternalDecompressorUseManifest {
310+
args = append(args, "--external-decompressor-use-manifest")
311+
}
308312
if cDetails.ManifestExternalDecompressorCmd != "" {
309313
args = append(args, fmt.Sprintf("--manifest-external-decompressor=%s", cDetails.ManifestExternalDecompressorCmd))
310314
}

go/test/endtoend/backup/xtrabackup/xtrabackup_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func TestXtrabackupWithExternalZstdCompressionAndManifestedDecompressor(t *testi
5959
CompressorEngineName: "external",
6060
ExternalCompressorCmd: "zstd",
6161
ExternalCompressorExt: ".zst",
62+
ExternalDecompressorUseManifest: true,
6263
ManifestExternalDecompressorCmd: "zstd -d",
6364
}
6465

go/vt/hook/hook.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ import (
2323
"io"
2424
"os"
2525
"os/exec"
26-
"path"
26+
"path/filepath"
2727
"strings"
2828
"syscall"
2929
"time"
3030

31+
"vitess.io/vitess/go/fileutil"
3132
vtenv "vitess.io/vitess/go/vt/env"
3233
"vitess.io/vitess/go/vt/log"
3334
)
@@ -98,19 +99,17 @@ func NewHookWithEnv(name string, params []string, env map[string]string) *Hook {
9899

99100
// findHook tries to locate the hook, and returns the exec.Cmd for it.
100101
func (hook *Hook) findHook(ctx context.Context) (*exec.Cmd, int, error) {
101-
// Check the hook path.
102-
if strings.Contains(hook.Name, "/") {
103-
return nil, HOOK_INVALID_NAME, fmt.Errorf("hook cannot contain '/'")
104-
}
105-
106102
// Find our root.
107103
root, err := vtenv.VtRoot()
108104
if err != nil {
109105
return nil, HOOK_VTROOT_ERROR, fmt.Errorf("cannot get VTROOT: %v", err)
110106
}
111107

112108
// See if the hook exists.
113-
vthook := path.Join(root, "vthook", hook.Name)
109+
vthook, err := fileutil.SafePathJoin(filepath.Join(root, "vthook"), hook.Name)
110+
if err != nil {
111+
return nil, HOOK_INVALID_NAME, fmt.Errorf("invalid hook name %q: %v", hook.Name, err)
112+
}
114113
_, err = os.Stat(vthook)
115114
if err != nil {
116115
if os.IsNotExist(err) {

go/vt/mysqlctl/blackbox/backup_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package blackbox
2020
import (
2121
"bytes"
2222
"context"
23+
"encoding/json"
2324
"errors"
2425
"fmt"
2526
"io"
@@ -926,3 +927,60 @@ func TestExecuteRestoreFailToReadEachFileTwice(t *testing.T) {
926927
require.Equal(t, 5, ss.SourceOpenStats)
927928
require.Equal(t, 5, ss.SourceReadStats)
928929
}
930+
931+
func TestBackupRetryPropagatesHashToManifest(t *testing.T) {
932+
ctx := utils.LeakCheckContext(t)
933+
backupRoot, keyspace, shard, ts := SetupCluster(ctx, t, 2, 2)
934+
935+
bufferPerFiles := make(map[string]*rwCloseFailFirstCall)
936+
be := &mysqlctl.BuiltinBackupEngine{}
937+
bh := &mysqlctl.FakeBackupHandle{}
938+
bh.AddFileReturnF = func(filename string) mysqlctl.FakeBackupHandleAddFileReturn {
939+
_, isRetry := bufferPerFiles[filename]
940+
newBuffer := newWriteCloseFailFirstWrite(isRetry)
941+
bufferPerFiles[filename] = newBuffer
942+
return mysqlctl.FakeBackupHandleAddFileReturn{WriteCloser: newBuffer}
943+
}
944+
945+
fakedb := fakesqldb.New(t)
946+
defer fakedb.Close()
947+
mysqld := mysqlctl.NewFakeMysqlDaemon(fakedb)
948+
defer mysqld.Close()
949+
mysqld.ExpectedExecuteSuperQueryList = []string{"STOP REPLICA", "START REPLICA"}
950+
951+
ctx, cancel := context.WithCancel(ctx)
952+
backupResult, err := be.ExecuteBackup(ctx, mysqlctl.BackupParams{
953+
Logger: logutil.NewMemoryLogger(),
954+
Mysqld: mysqld,
955+
Cnf: &mysqlctl.Mycnf{
956+
InnodbDataHomeDir: path.Join(backupRoot, "innodb"),
957+
InnodbLogGroupHomeDir: path.Join(backupRoot, "log"),
958+
DataDir: path.Join(backupRoot, "datadir"),
959+
},
960+
Stats: backupstats.NewFakeStats(),
961+
Concurrency: 1,
962+
HookExtraEnv: map[string]string{},
963+
TopoServer: ts,
964+
Keyspace: keyspace,
965+
Shard: shard,
966+
MysqlShutdownTimeout: MysqlShutdownTimeout,
967+
}, bh)
968+
cancel()
969+
970+
require.NoError(t, err)
971+
require.Equal(t, mysqlctl.BackupUsable, backupResult)
972+
973+
// Parse the MANIFEST and verify all file entries have non-empty hashes.
974+
manifestBuf, ok := bufferPerFiles["MANIFEST"]
975+
require.True(t, ok, "MANIFEST should have been written")
976+
977+
var manifest struct {
978+
FileEntries []mysqlctl.FileEntry
979+
}
980+
require.NoError(t, json.Unmarshal(manifestBuf.Bytes(), &manifest))
981+
982+
require.NotEmpty(t, manifest.FileEntries, "manifest should contain file entries")
983+
for _, fe := range manifest.FileEntries {
984+
assert.NotEmptyf(t, fe.Hash, "file %s should have a non-empty hash in the manifest", fe.Name)
985+
}
986+
}

go/vt/mysqlctl/builtinbackupengine.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,13 @@ func (be *BuiltinBackupEngine) backupFiles(
657657
if err != nil {
658658
return err
659659
}
660+
// Propagate retry results back to the original entries so the
661+
// manifest records correct hashes and metadata.
662+
for i, fe := range newFEs {
663+
if fe.Name != "" {
664+
fes[i] = fe
665+
}
666+
}
660667
}
661668

662669
// Backup the MANIFEST file and apply retry logic.
@@ -1327,10 +1334,7 @@ func (be *BuiltinBackupEngine) restoreFile(ctx context.Context, params RestorePa
13271334
// for backward compatibility
13281335
deCompressionEngine = PgzipCompressor
13291336
}
1330-
externalDecompressorCmd := ExternalDecompressorCmd
1331-
if externalDecompressorCmd == "" && bm.ExternalDecompressor != "" {
1332-
externalDecompressorCmd = bm.ExternalDecompressor
1333-
}
1337+
externalDecompressorCmd := resolveExternalDecompressor(bm.ExternalDecompressor)
13341338
if externalDecompressorCmd != "" {
13351339
if deCompressionEngine == ExternalCompressor {
13361340
deCompressionEngine = externalDecompressorCmd

0 commit comments

Comments
 (0)