Description
mutate.Extract() and crane export currently preserve tar entries that use Windows-style backslash separators, including entries that would be interpreted as traversal paths by Windows-aware or cross-platform archive extractors.
For example, when mutate.Extract() / crane export is run on Linux, an image layer entry such as:
..\..\tmp\GCR_CRANE_EXPORT_WINDOWS_BACKSLASH
is preserved in the resulting flattened filesystem tar stream as:
..\\..\\tmp\\GCR_CRANE_EXPORT_WINDOWS_BACKSLASH
On Linux this is treated as a normal filename because \ is not a path separator. However, on Windows or in archive extraction code that normalizes backslashes as separators, the same entry may resolve as:
../../tmp/GCR_CRANE_EXPORT_WINDOWS_BACKSLASH
This can allow the exported tar stream to contain paths that are unsafe for downstream extraction workflows.
I understand that crane export and mutate.Extract() do not themselves extract files to the local filesystem, so this is not necessarily a direct vulnerability in go-containerregistry. I am opening this as a hardening / defense-in-depth issue to discuss whether archive-safe path validation should be applied before emitting flattened filesystem tar streams.
Why this matters
mutate.Extract() is documented as flattening an image filesystem into a single tar stream and being the underlying implementation of crane export.
Downstream users may reasonably treat the output of crane export / mutate.Extract() as a normalized flattened filesystem representation of an image. If that output is later extracted on Windows, or by a cross-platform extractor that treats backslashes as path separators, preserved Windows-style traversal entries can escape the intended extraction root.
This creates a cross-platform TarSlip risk in downstream workflows that consume crane export output.
Reproducer
The following local reproducer creates an image layer containing a Windows-style traversal path, then exports it through pkg/crane.Export().
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"log"
"os"
cranelib "github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/tarball"
)
func addFile(tw *tar.Writer, name, body string) error {
h := &tar.Header{
Name: name,
Typeflag: tar.TypeReg,
Mode: 0644,
Size: int64(len(body)),
}
if err := tw.WriteHeader(h); err != nil {
return err
}
_, err := tw.Write([]byte(body))
return err
}
func makeLayer() ([]byte, error) {
var raw bytes.Buffer
tw := tar.NewWriter(&raw)
if err := addFile(tw, `..\..\tmp\GCR_CRANE_EXPORT_WINDOWS_BACKSLASH`, "WINDOWS_BACKSLASH_TARSLIP\n"); err != nil {
return nil, err
}
if err := addFile(tw, `safe\ok.txt`, "OK\n"); err != nil {
return nil, err
}
if err := tw.Close(); err != nil {
return nil, err
}
var gz bytes.Buffer
zw := gzip.NewWriter(&gz)
if _, err := zw.Write(raw.Bytes()); err != nil {
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return gz.Bytes(), nil
}
func main() {
layerBytes, err := makeLayer()
if err != nil {
log.Fatal(err)
}
layer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(layerBytes)), nil
})
if err != nil {
log.Fatal(err)
}
img, err := mutate.AppendLayers(empty.Image, layer)
if err != nil {
log.Fatal(err)
}
out, err := os.Create("/tmp/gcr_crane_export_windows_backslash.tar")
if err != nil {
log.Fatal(err)
}
defer out.Close()
if err := cranelib.Export(img, out); err != nil {
log.Fatal(err)
}
log.Println("wrote /tmp/gcr_crane_export_windows_backslash.tar via pkg/crane.Export")
}
Run:
go run /tmp/gcr_crane_export_windows_backslash_poc.go
tar -tvf /tmp/gcr_crane_export_windows_backslash.tar
Observed:
-rw-r--r-- 0/0 26 1970-01-01 01:00 ..\\..\\tmp\\GCR_CRANE_EXPORT_WINDOWS_BACKSLASH
-rw-r--r-- 0/0 3 1970-01-01 01:00 safe\\ok.txt
Expected behavior
It would be safer if mutate.Extract() / crane export rejected or normalized archive paths that are unsafe across platforms before emitting them into the flattened filesystem tar stream.
At minimum, validation could treat both / and \ as separators for safety checks and reject:
- absolute paths
- paths equal to
.. or starting with ../ after normalization
- Windows drive paths such as
C:\foo or C:/foo
- drive-relative paths such as
C:..\foo
- unsafe hardlink or symlink link targets, where applicable
Possible remediation
Use a platform-independent archive path validator before writing entries to the extracted/exported/flattened tar stream.
For example:
func unsafeArchivePath(p string) bool {
p = strings.ReplaceAll(p, "\\", "/")
clean := path.Clean(p)
if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
return true
}
if path.IsAbs(clean) {
return true
}
if len(clean) >= 2 &&
((clean[0] >= 'A' && clean[0] <= 'Z') || (clean[0] >= 'a' && clean[0] <= 'z')) &&
clean[1] == ':' {
return true
}
return false
}
Alternatively, the project could document clearly that mutate.Extract() / crane export output is not guaranteed to be safe for direct extraction by downstream tools without additional path validation.
Context
This was originally reported through Google VRP and was not considered severe enough to track as a security vulnerability because crane export and mutate.Extract() do not extract files to the local filesystem themselves. I am opening this public issue as a regular hardening / defense-in-depth request.
go_containerregistry_windows_tarslip_report_evidence.txt
Description
mutate.Extract()andcrane exportcurrently preserve tar entries that use Windows-style backslash separators, including entries that would be interpreted as traversal paths by Windows-aware or cross-platform archive extractors.For example, when
mutate.Extract()/crane exportis run on Linux, an image layer entry such as:is preserved in the resulting flattened filesystem tar stream as:
On Linux this is treated as a normal filename because
\is not a path separator. However, on Windows or in archive extraction code that normalizes backslashes as separators, the same entry may resolve as:This can allow the exported tar stream to contain paths that are unsafe for downstream extraction workflows.
I understand that
crane exportandmutate.Extract()do not themselves extract files to the local filesystem, so this is not necessarily a direct vulnerability ingo-containerregistry. I am opening this as a hardening / defense-in-depth issue to discuss whether archive-safe path validation should be applied before emitting flattened filesystem tar streams.Why this matters
mutate.Extract()is documented as flattening an image filesystem into a single tar stream and being the underlying implementation ofcrane export.Downstream users may reasonably treat the output of
crane export/mutate.Extract()as a normalized flattened filesystem representation of an image. If that output is later extracted on Windows, or by a cross-platform extractor that treats backslashes as path separators, preserved Windows-style traversal entries can escape the intended extraction root.This creates a cross-platform TarSlip risk in downstream workflows that consume
crane exportoutput.Reproducer
The following local reproducer creates an image layer containing a Windows-style traversal path, then exports it through
pkg/crane.Export().Run:
Observed:
Expected behavior
It would be safer if
mutate.Extract()/crane exportrejected or normalized archive paths that are unsafe across platforms before emitting them into the flattened filesystem tar stream.At minimum, validation could treat both
/and\as separators for safety checks and reject:..or starting with../after normalizationC:\fooorC:/fooC:..\fooPossible remediation
Use a platform-independent archive path validator before writing entries to the extracted/exported/flattened tar stream.
For example:
Alternatively, the project could document clearly that
mutate.Extract()/crane exportoutput is not guaranteed to be safe for direct extraction by downstream tools without additional path validation.Context
This was originally reported through Google VRP and was not considered severe enough to track as a security vulnerability because
crane exportandmutate.Extract()do not extract files to the local filesystem themselves. I am opening this public issue as a regular hardening / defense-in-depth request.go_containerregistry_windows_tarslip_report_evidence.txt