VYPR
Critical severityNVD Advisory· Published Feb 26, 2026· Updated Feb 26, 2026

Vitess users with backup storage access can write to arbitrary file paths on restore

CVE-2026-27969

Description

Vitess is a database clustering system for horizontal scaling of MySQL. Prior to versions 23.0.3 and 22.0.4, anyone with read/write access to the backup storage location (e.g. an S3 bucket) can manipulate backup manifest files so that files in the manifest — which may be files that they have also added to the manifest and backup contents — are written to any accessible location on restore. This is a common path traversal security issue. This can be used to provide that attacker with unintended/unauthorized access to the production deployment environment — allowing them to access information available in that environment as well as run any additional arbitrary commands there. Versions 23.0.3 and 22.0.4 contain a patch. No known workarounds are available.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Vitess backup restore allows path traversal via manipulated manifest files, enabling attackers with storage access to write files to arbitrary locations.

Vulnerability

Vitess backup and restore process trusts the content of backup manifest files (MANIFEST) stored in backup storage locations such as S3 buckets. An attacker with read/write access to that storage can craft a manifest containing path traversal sequences (e.g., ../). During restore, the software constructs file paths by combining base directories with names from the manifest without proper validation, leading to arbitrary file write outside the intended restore directory [1]. This is a classic path traversal security issue [2].

Exploitation

To exploit this vulnerability, an attacker must have read and write access to the backup storage location (e.g., an S3 bucket) used by Vitess. They can modify the MANIFEST file to include entries with path traversal sequences and add corresponding files to the backup contents. No additional authentication or privileges on the Vitess cluster are required; the traversal occurs when the backup is restored, for example during disaster recovery procedures [1].

Impact

Successful exploitation allows the attacker to write arbitrary files to any accessible path on the filesystem of the target environment where the restore is performed. This can lead to unauthorized access to sensitive information, overwriting critical configuration files, or placement of malicious executables that enable further compromise. The patch notes indicate that this could allow the attacker to run arbitrary commands in the production deployment environment [1].

Mitigation

The vulnerability is fixed in Vitess versions 23.0.3 and 22.0.4. The fix adds validation in the backup engine to reject file entries that would escape the designated base directory using path traversal [3][4]. The commit introduces a fileutil.ErrInvalidJoinedPath error for such attempts. No known workarounds exist, so upgrading to a patched version is strongly recommended [1].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vitess.io/vitessGo
>= 0.23.0-rc1, < 0.23.30.23.3
vitess.io/vitessGo
< 0.22.40.22.4

Affected products

2
  • Vitess/Vitessllm-fuzzy
    Range: prior to 22.0.4 (22.x) and prior to 23.0.3 (23.x)
  • vitessio/vitessv5
    Range: < 22.0.4

Patches

1
c565cab615bc

`backupengine`: disallow path traversals via backup `MANIFEST` on restore (#19470)

https://github.com/vitessio/vitessTim VaillancourtFeb 25, 2026via ghsa
2 files changed · +94 2
  • go/vt/mysqlctl/builtinbackupengine.go+5 2 modified
    @@ -36,6 +36,7 @@ import (
     	"github.com/spf13/pflag"
     	"golang.org/x/sync/errgroup"
     
    +	"vitess.io/vitess/go/fileutil"
     	"vitess.io/vitess/go/ioutil"
     	"vitess.io/vitess/go/mysql"
     	"vitess.io/vitess/go/mysql/replication"
    @@ -180,7 +181,9 @@ func registerBuiltinBackupEngineFlags(fs *pflag.FlagSet) {
     	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.")
     }
     
    -// fullPath returns the full path of the entry, based on its type
    +// fullPath returns the full path of the entry, based on its type.
    +// It validates that the resolved path does not escape the base directory
    +// via path traversal (e.g. "../../" sequences in fe.Name).
     func (fe *FileEntry) fullPath(cnf *Mycnf) (string, error) {
     	// find the root to use
     	var root string
    @@ -197,7 +200,7 @@ func (fe *FileEntry) fullPath(cnf *Mycnf) (string, error) {
     		return "", vterrors.Errorf(vtrpcpb.Code_UNKNOWN, "unknown base: %v", fe.Base)
     	}
     
    -	return path.Join(fe.ParentPath, root, fe.Name), nil
    +	return fileutil.SafePathJoin(path.Join(fe.ParentPath, root), fe.Name)
     }
     
     // open attempts to open the file
    
  • go/vt/mysqlctl/builtinbackupengine_test.go+89 0 modified
    @@ -21,7 +21,9 @@ import (
     	"testing"
     
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/require"
     
    +	"vitess.io/vitess/go/fileutil"
     	tabletmanagerdatapb "vitess.io/vitess/go/vt/proto/tabletmanagerdata"
     )
     
    @@ -70,6 +72,93 @@ func TestGetIncrementalFromPosGTIDSet(t *testing.T) {
     	}
     }
     
    +func TestFileEntryFullPath(t *testing.T) {
    +	cnf := &Mycnf{
    +		DataDir:               "/vt/data",
    +		InnodbDataHomeDir:     "/vt/innodb-data",
    +		InnodbLogGroupHomeDir: "/vt/innodb-log",
    +		BinLogPath:            "/vt/binlogs/mysql-bin",
    +	}
    +
    +	tests := []struct {
    +		name      string
    +		entry     FileEntry
    +		wantPath  string
    +		wantError error
    +	}{
    +		{
    +			name:     "valid relative path in DataDir",
    +			entry:    FileEntry{Base: backupData, Name: "mydb/table1.ibd"},
    +			wantPath: "/vt/data/mydb/table1.ibd",
    +		},
    +		{
    +			name:     "valid relative path in InnodbDataHomeDir",
    +			entry:    FileEntry{Base: backupInnodbDataHomeDir, Name: "ibdata1"},
    +			wantPath: "/vt/innodb-data/ibdata1",
    +		},
    +		{
    +			name:     "valid relative path in InnodbLogGroupHomeDir",
    +			entry:    FileEntry{Base: backupInnodbLogGroupHomeDir, Name: "ib_logfile0"},
    +			wantPath: "/vt/innodb-log/ib_logfile0",
    +		},
    +		{
    +			name:     "valid relative path in BinlogDir",
    +			entry:    FileEntry{Base: backupBinlogDir, Name: "mysql-bin.000001"},
    +			wantPath: "/vt/binlogs/mysql-bin.000001",
    +		},
    +		{
    +			name:     "valid path with ParentPath",
    +			entry:    FileEntry{Base: backupData, Name: "mydb/table1.ibd", ParentPath: "/tmp/restore"},
    +			wantPath: "/tmp/restore/vt/data/mydb/table1.ibd",
    +		},
    +		{
    +			name:      "path traversal escapes base directory",
    +			entry:     FileEntry{Base: backupData, Name: "../../etc/passwd"},
    +			wantError: fileutil.ErrInvalidJoinedPath,
    +		},
    +		{
    +			name:      "path traversal with deeper nesting",
    +			entry:     FileEntry{Base: backupData, Name: "mydb/../../../etc/shadow"},
    +			wantError: fileutil.ErrInvalidJoinedPath,
    +		},
    +		{
    +			name:      "path traversal to root",
    +			entry:     FileEntry{Base: backupData, Name: "../../../../../etc/crontab"},
    +			wantError: fileutil.ErrInvalidJoinedPath,
    +		},
    +		{
    +			name:      "path traversal escapes ParentPath",
    +			entry:     FileEntry{Base: backupData, Name: "../../../../etc/passwd", ParentPath: "/tmp/restore"},
    +			wantError: fileutil.ErrInvalidJoinedPath,
    +		},
    +		{
    +			name:     "relative path with dot-dot that stays within base",
    +			entry:    FileEntry{Base: backupData, Name: "mydb/../mydb/table1.ibd"},
    +			wantPath: "/vt/data/mydb/table1.ibd",
    +		},
    +	}
    +
    +	// Test unknown base separately since it returns a different error type.
    +	t.Run("unknown base", func(t *testing.T) {
    +		entry := FileEntry{Base: "unknown", Name: "file"}
    +		_, err := entry.fullPath(cnf)
    +		require.Error(t, err)
    +		assert.Contains(t, err.Error(), "unknown base")
    +	})
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			got, err := tt.entry.fullPath(cnf)
    +			if tt.wantError != nil {
    +				require.ErrorIs(t, err, tt.wantError)
    +			} else {
    +				require.NoError(t, err)
    +				assert.Equal(t, tt.wantPath, got)
    +			}
    +		})
    +	}
    +}
    +
     func TestShouldDrainForBackupBuiltIn(t *testing.T) {
     	be := &BuiltinBackupEngine{}
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.