Vitess users with backup storage access can gain unauthorized access to production deployment environments
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 arbitrary code is later executed when that backup is restored. 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. Some workarounds are available. Those who intended to use an external decompressor then can always specify that decompressor command in the --external-decompressor flag value for vttablet and vtbackup. That then overrides any value specified in the manifest file. Those who did not intend to use an external decompressor, nor an internal one, can specify a value such as cat or tee in the --external-decompressor flag value for vttablet and vtbackup to ensure that a harmless command is always used.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2026-27965: An attacker with write access to Vitess backup storage can execute arbitrary code on tablets by injecting malicious compressor commands into backup manifest files.
Vulnerability
Description
CVE-2026-27965 is a critical security vulnerability in Vitess, a database clustering system for horizontal scaling of MySQL. Prior to versions 23.0.3 and 22.0.4, Vitess stored the external decompressor command and arguments in the backup manifest file (MANIFEST) alongside backup data. When a backup is restored without an explicit --external-decompressor flag provided to vttablet or vtbackup, the commands from the manifest are used directly for decompression. This design allows an attacker with read/write access to the backup storage location (e.g., an S3 bucket) to modify the manifest file and inject arbitrary commands, which will then be executed by the tablet during restoration [1][4].
Attack
Vector and Exploitation
Exploitation requires two conditions: the attacker must have write access to the backup storage (e.g., modifying files in an S3 bucket), and they must be able to trigger a restore of the tampered backup (directly or indirectly, such as through automated restore workflows). By altering the MANIFESTMANIFEST` file to specify a malicious command (e.g., bash with -c` arguments pointing to a reverse shell script), the attacker can execute arbitrary code on the tablet host during the decompressing the backup. The vulnerability exists because therfore enables privilege escalation from storage access to code execution on the Vitess deployment environment [4].
Impact
A successful attack grants the attacker arbitrary code execution in the context of the Vitess user on the tablet. This can lead to full compromise of the production deployment environment, including unauthorized access to sensitive data, further lateral movement within the infrastructure, and the ability to run additional commands or deploy malware [1].
Mitigation and
Patches
The official patch (commits in pull request #19460) introduces a new flag --external-decompressor-use-manifest, which is set to false by default. This makes the use of manifest-supplied decompressor commands opt-in, removing the default trust behavior [2][3]. The error message for unsupported decompression engines now warns against enabling this flag, noting the security risk [3]. As a workaround, operators can explicitly specify a value for --external-decompressor (such as cat or tee) to override any command in the manifest, ensuring only a harmless command is used [1]. Users should upgrade to Vitess versions 23.0.3 or 22.0.4 immediately, or apply one of the workarounds outlined in the advisory.
- NVD - CVE-2026-27965
- Restore: make loading compressor commands from `MANIFEST` opt-in by timvaillancourt · Pull Request #19460 · vitessio/vitess
- Restore: make loading compressor commands from `MANIFEST` opt-in (#19… · vitessio/vitess@4c01732
- Bug Report: backup restore trusts decompressor in `MANIFEST` by default
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.
| Package | Affected versions | Patched versions |
|---|---|---|
vitess.io/vitessGo | <= 0.23.2 | — |
Affected products
2- vitessio/vitessv5Range: < 22.0.4
Patches
14c0173293907Restore: make loading compressor commands from `MANIFEST` opt-in (#19460)
11 files changed · +122 −9
go/flags/endtoend/vtbackup.txt+1 −0 modified@@ -136,6 +136,7 @@ Flags: --external-compressor string command with arguments to use when compressing a backup. --external-compressor-extension string extension to use when using an external compressor. --external-decompressor string command with arguments to use when decompressing a backup. + --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. --file-backup-storage-root string Root directory for the file backup storage. --gcs-backup-storage-bucket string Google Cloud Storage bucket to use for backups. --gcs-backup-storage-root string Root prefix for all backup-related object names.
go/flags/endtoend/vtcombo.txt+1 −0 modified@@ -134,6 +134,7 @@ Flags: --external-compressor string command with arguments to use when compressing a backup. --external-compressor-extension string extension to use when using an external compressor. --external-decompressor string command with arguments to use when decompressing a backup. + --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. --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 --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") --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+1 −0 modified@@ -158,6 +158,7 @@ Flags: --external-compressor string command with arguments to use when compressing a backup. --external-compressor-extension string extension to use when using an external compressor. --external-decompressor string command with arguments to use when decompressing a backup. + --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. --file-backup-storage-root string Root directory for the file backup storage. --filecustomrules string file based custom rule path --filecustomrules-watch set up a watch on the target file and reload query rules when it changes
go/flags/endtoend/vttestserver.txt+1 −0 modified@@ -40,6 +40,7 @@ Flags: --external-compressor string command with arguments to use when compressing a backup. --external-compressor-extension string extension to use when using an external compressor. --external-decompressor string command with arguments to use when decompressing a backup. + --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. --external-topo-global-root string the path of the global topology data in the global topology server for vtcombo process --external-topo-global-server-address string the address of the global topology server for vtcombo process --external-topo-implementation string the topology implementation to use for vtcombo process
go/test/endtoend/backup/vtctlbackup/backup_test.go+1 −0 modified@@ -60,6 +60,7 @@ func TestBuiltinBackupWithExternalZstdCompressionAndManifestedDecompressor(t *te CompressorEngineName: "external", ExternalCompressorCmd: "zstd", ExternalCompressorExt: ".zst", + ExternalDecompressorUseManifest: true, ManifestExternalDecompressorCmd: "zstd -d", }
go/test/endtoend/backup/vtctlbackup/backup_utils.go+4 −0 modified@@ -94,6 +94,7 @@ type CompressionDetails struct { ExternalCompressorCmd string ExternalCompressorExt string ExternalDecompressorCmd string + ExternalDecompressorUseManifest bool ManifestExternalDecompressorCmd string } @@ -307,6 +308,9 @@ func getCompressorArgs(cDetails *CompressionDetails) []string { if cDetails.ExternalDecompressorCmd != "" { args = append(args, "--external-decompressor="+cDetails.ExternalDecompressorCmd) } + if cDetails.ExternalDecompressorUseManifest { + args = append(args, "--external-decompressor-use-manifest") + } if cDetails.ManifestExternalDecompressorCmd != "" { args = append(args, "--manifest-external-decompressor="+cDetails.ManifestExternalDecompressorCmd) }
go/test/endtoend/backup/xtrabackup/xtrabackup_test.go+1 −0 modified@@ -59,6 +59,7 @@ func TestXtrabackupWithExternalZstdCompressionAndManifestedDecompressor(t *testi CompressorEngineName: "external", ExternalCompressorCmd: "zstd", ExternalCompressorExt: ".zst", + ExternalDecompressorUseManifest: true, ManifestExternalDecompressorCmd: "zstd -d", }
go/vt/mysqlctl/builtinbackupengine.go+1 −4 modified@@ -1329,10 +1329,7 @@ func (be *BuiltinBackupEngine) restoreFile(ctx context.Context, params RestorePa // for backward compatibility deCompressionEngine = PgzipCompressor } - externalDecompressorCmd := ExternalDecompressorCmd - if externalDecompressorCmd == "" && bm.ExternalDecompressor != "" { - externalDecompressorCmd = bm.ExternalDecompressor - } + externalDecompressorCmd := resolveExternalDecompressor(bm.ExternalDecompressor) if externalDecompressorCmd != "" { if deCompressionEngine == ExternalCompressor { deCompressionEngine = externalDecompressorCmd
go/vt/mysqlctl/compression_external_decompressor_test.go+93 −0 added@@ -0,0 +1,93 @@ +/* +Copyright 2026 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mysqlctl + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolveExternalDecompressor(t *testing.T) { + tests := []struct { + name string + cliDecompressorCmd string + useManifest bool + manifestDecompressor string + expected string + }{ + { + name: "CLI flag takes precedence over manifest", + cliDecompressorCmd: "zstd -d", + useManifest: true, + manifestDecompressor: "gzip -d", + expected: "zstd -d", + }, + { + name: "CLI flag takes precedence even when use-manifest is false", + cliDecompressorCmd: "zstd -d", + useManifest: false, + manifestDecompressor: "gzip -d", + expected: "zstd -d", + }, + { + name: "manifest used when use-manifest is true and no CLI flag", + cliDecompressorCmd: "", + useManifest: true, + manifestDecompressor: "gzip -d", + expected: "gzip -d", + }, + { + name: "manifest ignored when use-manifest is false", + cliDecompressorCmd: "", + useManifest: false, + manifestDecompressor: "gzip -d", + expected: "", + }, + { + name: "empty when nothing is set", + cliDecompressorCmd: "", + useManifest: false, + manifestDecompressor: "", + expected: "", + }, + { + name: "empty when use-manifest is true but manifest is empty", + cliDecompressorCmd: "", + useManifest: true, + manifestDecompressor: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + origCmd := ExternalDecompressorCmd + origAllow := ExternalDecompressorUseManifest + t.Cleanup(func() { + ExternalDecompressorCmd = origCmd + ExternalDecompressorUseManifest = origAllow + }) + + ExternalDecompressorCmd = tt.cliDecompressorCmd + ExternalDecompressorUseManifest = tt.useManifest + + result := resolveExternalDecompressor(tt.manifestDecompressor) + assert.Equal(t, tt.expected, result) + }) + } +}
go/vt/mysqlctl/compression.go+17 −1 modified@@ -53,9 +53,10 @@ var ( ExternalCompressorCmd string ExternalCompressorExt string ExternalDecompressorCmd string + ExternalDecompressorUseManifest bool ManifestExternalDecompressorCmd string - errUnsupportedDeCompressionEngine = errors.New("unsupported engine in MANIFEST. You need to provide --external-decompressor if using 'external' compression engine") + errUnsupportedDeCompressionEngine = errors.New("unsupported engine in MANIFEST. You need to provide --external-decompressor if using 'external' compression engine. Alternatively, set --external-decompressor-use-manifest to use the decompressor command from the backup manifest, but this is NOT RECOMMENDED as it is a security risk") errUnsupportedCompressionEngine = errors.New("unsupported engine value for --compression-engine-name. supported values are 'external', 'pgzip', 'pargzip', 'zstd', 'lz4'") // this is used by getEngineFromExtension() to figure out which engine to use in case the user didn't specify @@ -78,6 +79,7 @@ func registerBackupCompressionFlags(fs *pflag.FlagSet) { fs.StringVar(&ExternalCompressorCmd, "external-compressor", ExternalCompressorCmd, "command with arguments to use when compressing a backup.") fs.StringVar(&ExternalCompressorExt, "external-compressor-extension", ExternalCompressorExt, "extension to use when using an external compressor.") fs.StringVar(&ExternalDecompressorCmd, "external-decompressor", ExternalDecompressorCmd, "command with arguments to use when decompressing a backup.") + fs.BoolVar(&ExternalDecompressorUseManifest, "external-decompressor-use-manifest", ExternalDecompressorUseManifest, "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.") fs.StringVar(&ManifestExternalDecompressorCmd, "manifest-external-decompressor", ManifestExternalDecompressorCmd, "command with arguments to store in the backup manifest when compressing a backup with an external compression engine.") } @@ -90,6 +92,20 @@ func getExtensionFromEngine(engine string) (string, error) { return "", fmt.Errorf("%w %q", errUnsupportedCompressionEngine, engine) } +// resolveExternalDecompressor returns the external decompressor command to use +// at restore time. The CLI flag (--external-decompressor) takes precedence. The +// backup manifest value is only used when --external-decompressor-use-manifest +// is explicitly set to true. +func resolveExternalDecompressor(manifestDecompressor string) string { + if ExternalDecompressorCmd != "" { + return ExternalDecompressorCmd + } + if ExternalDecompressorUseManifest && manifestDecompressor != "" { + return manifestDecompressor + } + return "" +} + // Validates if the external decompressor exists and return its path. func validateExternalCmd(cmd string) (string, error) { if cmd == "" {
go/vt/mysqlctl/xtrabackupengine.go+1 −4 modified@@ -649,10 +649,7 @@ func (be *XtrabackupEngine) extractFiles(ctx context.Context, logger logutil.Log // then we assign the default value of compressionEngine. deCompressionEngine = PgzipCompressor } - externalDecompressorCmd := ExternalDecompressorCmd - if externalDecompressorCmd == "" && bm.ExternalDecompressor != "" { - externalDecompressorCmd = bm.ExternalDecompressor - } + externalDecompressorCmd := resolveExternalDecompressor(bm.ExternalDecompressor) if externalDecompressorCmd != "" { if deCompressionEngine == ExternalCompressor { deCompressionEngine = externalDecompressorCmd
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-8g8j-r87h-p36xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27965ghsaADVISORY
- github.com/vitessio/vitess/commit/4c0173293907af9cb942a6683c465c3f1e9fdb5cghsax_refsource_MISCWEB
- github.com/vitessio/vitess/issues/19459ghsax_refsource_MISCWEB
- github.com/vitessio/vitess/pull/19460ghsax_refsource_MISCWEB
- github.com/vitessio/vitess/security/advisories/GHSA-8g8j-r87h-p36xghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.