Skip to content

Commit df12d88

Browse files
Merge pull request #2291 from rhamitarora/rhamitarora/ARO-26358-codeql-fix
NO-JIRA: Fix tar slip path traversal in codesign archive extraction
2 parents 74e525a + 15da98f commit df12d88

3 files changed

Lines changed: 125 additions & 10 deletions

File tree

pkg/cli/admin/internal/codesign/machoresign.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"archive/tar"
55
"compress/gzip"
66
"debug/macho"
7+
"fmt"
78
"io"
89
"io/fs"
910
"os"
1011
"path/filepath"
12+
"strings"
1113
)
1214

1315
func ResignMacho(path string, asArchive bool, command string, links []string) error {
@@ -125,6 +127,7 @@ func extractTarToTmpAndSign(path string) (string, error) {
125127
if err != nil {
126128
return "", err
127129
}
130+
cleanTempDir := filepath.Clean(tempDir)
128131

129132
gzipFile, err := os.Open(path)
130133
if err != nil {
@@ -155,6 +158,15 @@ func extractTarToTmpAndSign(path string) (string, error) {
155158

156159
tempExtractionPath := filepath.Clean(filepath.Join(tempDir, header.Name))
157160

161+
// Guard against tar slip: ensure the resolved path stays within tempDir.
162+
// filepath.Clean/Join normalize but do not constrain the result, so an
163+
// entry name containing ".." could escape the extraction directory. The
164+
// safe branch is determined solely by strings.HasPrefix so that static
165+
// analyzers recognize this as a sanitizer.
166+
if !strings.HasPrefix(tempExtractionPath, cleanTempDir+string(os.PathSeparator)) {
167+
return "", fmt.Errorf("invalid archive entry %q: escapes extraction directory", header.Name)
168+
}
169+
158170
switch header.Typeflag {
159171
case tar.TypeReg:
160172
f, err := os.OpenFile(tempExtractionPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode).Perm())
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package codesign
2+
3+
import (
4+
"archive/tar"
5+
"compress/gzip"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
)
11+
12+
// writeTarGz writes a gzip-compressed tar archive at path containing the given
13+
// regular-file entries keyed by entry name.
14+
func writeTarGz(t *testing.T, path string, entries map[string][]byte) {
15+
t.Helper()
16+
17+
f, err := os.Create(path)
18+
if err != nil {
19+
t.Fatalf("creating archive: %v", err)
20+
}
21+
defer f.Close()
22+
23+
gw := gzip.NewWriter(f)
24+
tw := tar.NewWriter(gw)
25+
26+
for name, data := range entries {
27+
hdr := &tar.Header{
28+
Name: name,
29+
Mode: 0644,
30+
Size: int64(len(data)),
31+
Typeflag: tar.TypeReg,
32+
}
33+
if err := tw.WriteHeader(hdr); err != nil {
34+
t.Fatalf("writing header %q: %v", name, err)
35+
}
36+
if _, err := tw.Write(data); err != nil {
37+
t.Fatalf("writing data %q: %v", name, err)
38+
}
39+
}
40+
41+
if err := tw.Close(); err != nil {
42+
t.Fatalf("closing tar writer: %v", err)
43+
}
44+
if err := gw.Close(); err != nil {
45+
t.Fatalf("closing gzip writer: %v", err)
46+
}
47+
}
48+
49+
func TestExtractTarToTmpAndSign_RejectsPathTraversal(t *testing.T) {
50+
dir := t.TempDir()
51+
archivePath := filepath.Join(dir, "malicious.tar.gz")
52+
sentinel := filepath.Join(dir, "escaped.txt")
53+
54+
writeTarGz(t, archivePath, map[string][]byte{
55+
"../escaped.txt": []byte("owned"),
56+
})
57+
58+
tempDir, err := extractTarToTmpAndSign(archivePath)
59+
if tempDir != "" {
60+
defer os.RemoveAll(tempDir)
61+
}
62+
if err == nil {
63+
t.Fatal("expected an error for path-traversal entry, got nil")
64+
}
65+
if tempDir != "" {
66+
t.Fatalf("expected empty tempDir on rejection, got %q", tempDir)
67+
}
68+
69+
if _, statErr := os.Stat(sentinel); statErr == nil {
70+
t.Fatalf("path traversal succeeded: file written outside extraction dir at %s", sentinel)
71+
}
72+
}
73+
74+
func TestExtractTarToTmpAndSign_ExtractsRegularFile(t *testing.T) {
75+
dir := t.TempDir()
76+
archivePath := filepath.Join(dir, "benign.tar.gz")
77+
78+
writeTarGz(t, archivePath, map[string][]byte{
79+
"tool": []byte("not a macho binary"),
80+
})
81+
82+
tempDir, err := extractTarToTmpAndSign(archivePath)
83+
if tempDir != "" {
84+
defer os.RemoveAll(tempDir)
85+
}
86+
if err != nil {
87+
t.Fatalf("unexpected error: %v", err)
88+
}
89+
if tempDir == "" {
90+
t.Fatal("expected a non-empty tempDir on success")
91+
}
92+
if !strings.HasPrefix(filepath.Base(tempDir), "oc-release-extract-") {
93+
t.Fatalf("unexpected tempDir name: %q", tempDir)
94+
}
95+
96+
extracted := filepath.Join(tempDir, "tool")
97+
if _, statErr := os.Stat(extracted); statErr != nil {
98+
t.Fatalf("expected extracted file at %s: %v", extracted, statErr)
99+
}
100+
}

pkg/cli/image/archive/archive.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,19 @@ func unpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
141141
}
142142
}
143143

144+
// Ensure the resolved destination stays within dest before performing any
145+
// filesystem operation (including parent-directory creation below), so a
146+
// malicious entry containing '..' cannot escape the extraction directory.
147+
path := filepath.Join(dest, hdr.Name)
148+
rel, err := filepath.Rel(dest, path)
149+
if err != nil {
150+
return 0, err
151+
}
152+
// Note as these operations are platform specific, so must the slash be.
153+
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
154+
return 0, breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
155+
}
156+
144157
// Windows does not support filenames with colons in them. Ignore
145158
// these files. This is not a problem though (although it might
146159
// appear that it is). Let's suppose a client is running docker pull.
@@ -201,16 +214,6 @@ func unpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
201214
}
202215
}
203216

204-
path := filepath.Join(dest, hdr.Name)
205-
rel, err := filepath.Rel(dest, path)
206-
if err != nil {
207-
return 0, err
208-
}
209-
210-
// Note as these operations are platform specific, so must the slash be.
211-
if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
212-
return 0, breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
213-
}
214217
base := filepath.Base(path)
215218

216219
if strings.HasPrefix(base, archive.WhiteoutPrefix) {

0 commit comments

Comments
 (0)