Skip to content

Commit 96cd904

Browse files
shizhMSFTjdolitsky
andauthored
Merge pull request from GHSA-g5v4-5x39-vwhx
* check hard link * no following symbolic link * bug fix * add initial test to reproduce GHSA-g5v4-5x39-vwhx Signed-off-by: jdolitsky <393494+jdolitsky@users.noreply.github.com> * fix test for symbolic link * fix bug * add test for hardlink Signed-off-by: jdolitsky <393494+jdolitsky@users.noreply.github.com> * catch the parent folder * remove check for hard link for consistency * remove unncessary test for hard links * Revert "remove unncessary test for hard links" This reverts commit b3136611810f49074dfc6aef158b3d24466d2ed9. * Revert "remove check for hard link for consistency" This reverts commit d7b7346598c92ff9c430a42763d810b34d3f1ac2. * check links for all link types * add tests Co-authored-by: jdolitsky <393494+jdolitsky@users.noreply.github.com>
1 parent 200d032 commit 96cd904

File tree

2 files changed

+170
-4
lines changed

2 files changed

+170
-4
lines changed

pkg/content/utils.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,24 @@ func extractTarDirectory(root, prefix string, r io.Reader) error {
101101

102102
// Name check
103103
name := header.Name
104-
path, err := filepath.Rel(prefix, name)
104+
path, err := ensureBasePath(root, prefix, name)
105105
if err != nil {
106106
return err
107107
}
108-
if strings.HasPrefix(path, "../") {
109-
return fmt.Errorf("%q does not have prefix %q", name, prefix)
110-
}
111108
path = filepath.Join(root, path)
112109

110+
// Link check
111+
switch header.Typeflag {
112+
case tar.TypeLink, tar.TypeSymlink:
113+
link := header.Linkname
114+
if !filepath.IsAbs(link) {
115+
link = filepath.Join(filepath.Dir(name), link)
116+
}
117+
if _, err := ensureBasePath(root, prefix, link); err != nil {
118+
return err
119+
}
120+
}
121+
113122
// Create content
114123
switch header.Typeflag {
115124
case tar.TypeReg:
@@ -132,6 +141,34 @@ func extractTarDirectory(root, prefix string, r io.Reader) error {
132141
}
133142
}
134143

144+
// ensureBasePath ensures the target path is in the base path,
145+
// returning its relative path to the base path.
146+
func ensureBasePath(root, base, target string) (string, error) {
147+
path, err := filepath.Rel(base, target)
148+
if err != nil {
149+
return "", err
150+
}
151+
cleanPath := filepath.ToSlash(filepath.Clean(path))
152+
if cleanPath == ".." || strings.HasPrefix(cleanPath, "../") {
153+
return "", fmt.Errorf("%q is outside of %q", target, base)
154+
}
155+
156+
// No symbolic link allowed in the relative path
157+
dir := filepath.Dir(path)
158+
for dir != "." {
159+
if info, err := os.Lstat(filepath.Join(root, dir)); err != nil {
160+
if !os.IsNotExist(err) {
161+
return "", err
162+
}
163+
} else if info.Mode()&os.ModeSymlink != 0 {
164+
return "", fmt.Errorf("no symbolic link allowed between %q and %q", base, target)
165+
}
166+
dir = filepath.Dir(dir)
167+
}
168+
169+
return path, nil
170+
}
171+
135172
func writeFile(path string, r io.Reader, perm os.FileMode) error {
136173
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
137174
if err != nil {

pkg/oras/oras_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package oras
22

33
import (
4+
"archive/tar"
5+
"bytes"
6+
"compress/gzip"
47
"context"
8+
_ "crypto/sha256"
59
"fmt"
10+
"io"
611
"io/ioutil"
712
"os"
813
"path/filepath"
@@ -17,6 +22,7 @@ import (
1722
"github.com/docker/distribution/configuration"
1823
"github.com/docker/distribution/registry"
1924
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
25+
digest "github.com/opencontainers/go-digest"
2026
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2127
"github.com/phayes/freeport"
2228
"github.com/stretchr/testify/suite"
@@ -313,6 +319,129 @@ func (suite *ORASTestSuite) Test_3_Conditional_Pull() {
313319
}
314320
}
315321

322+
// Test for vulnerability GHSA-g5v4-5x39-vwhx
323+
func (suite *ORASTestSuite) Test_4_GHSA_g5v4_5x39_vwhx() {
324+
var testVulnerability = func(headers []tar.Header, tag string, expectedError string) {
325+
// Step 1: build malicious tar+gzip
326+
buf := bytes.NewBuffer(nil)
327+
digester := digest.Canonical.Digester()
328+
zw := gzip.NewWriter(io.MultiWriter(buf, digester.Hash()))
329+
tarDigester := digest.Canonical.Digester()
330+
tw := tar.NewWriter(io.MultiWriter(zw, tarDigester.Hash()))
331+
for _, header := range headers {
332+
err := tw.WriteHeader(&header)
333+
suite.Nil(err, "error writing header")
334+
}
335+
err := tw.Close()
336+
suite.Nil(err, "error closing tar")
337+
err = zw.Close()
338+
suite.Nil(err, "error closing gzip")
339+
340+
// Step 2: construct malicious descriptor
341+
evilDesc := ocispec.Descriptor{
342+
MediaType: ocispec.MediaTypeImageLayerGzip,
343+
Digest: digester.Digest(),
344+
Size: int64(buf.Len()),
345+
Annotations: map[string]string{
346+
orascontent.AnnotationDigest: tarDigester.Digest().String(),
347+
orascontent.AnnotationUnpack: "true",
348+
ocispec.AnnotationTitle: "foo",
349+
},
350+
}
351+
352+
// Step 3: upload malicious artifact to registry
353+
memoryStore := orascontent.NewMemoryStore()
354+
memoryStore.Set(evilDesc, buf.Bytes())
355+
ref := fmt.Sprintf("%s/evil:%s", suite.DockerRegistryHost, tag)
356+
_, err = Push(newContext(), newResolver(), ref, memoryStore, []ocispec.Descriptor{evilDesc})
357+
suite.Nil(err, "no error pushing test data")
358+
359+
// Step 4: pull malicious tar with oras filestore and ensure error
360+
tempDir, err := ioutil.TempDir("", "oras_test")
361+
if err != nil {
362+
suite.FailNow("error creating temp directory", err)
363+
}
364+
defer os.RemoveAll(tempDir)
365+
store := orascontent.NewFileStore(tempDir)
366+
defer store.Close()
367+
ref = fmt.Sprintf("%s/evil:%s", suite.DockerRegistryHost, tag)
368+
_, _, err = Pull(newContext(), newResolver(), ref, store)
369+
suite.NotNil(err, "error expected pulling malicious tar")
370+
suite.Contains(err.Error(),
371+
expectedError,
372+
"did not get correct error message",
373+
)
374+
}
375+
376+
tests := []struct {
377+
name string
378+
headers []tar.Header
379+
tag string
380+
expectedError string
381+
}{
382+
{
383+
name: "Test symbolic link path traversal",
384+
headers: []tar.Header{
385+
{
386+
Typeflag: tar.TypeDir,
387+
Name: "foo/subdir/",
388+
Mode: 0755,
389+
},
390+
{ // Symbolic link to `foo`
391+
Typeflag: tar.TypeSymlink,
392+
Name: "foo/subdir/parent",
393+
Linkname: "..",
394+
Mode: 0755,
395+
},
396+
{ // Symbolic link to `../etc/passwd`
397+
Typeflag: tar.TypeSymlink,
398+
Name: "foo/subdir/parent/passwd",
399+
Linkname: "../../etc/passwd",
400+
Mode: 0644,
401+
},
402+
{ // Symbolic link to `../etc`
403+
Typeflag: tar.TypeSymlink,
404+
Name: "foo/subdir/parent/etc",
405+
Linkname: "../../etc",
406+
Mode: 0644,
407+
},
408+
},
409+
tag: "symlink_path",
410+
expectedError: "no symbolic link allowed",
411+
},
412+
{
413+
name: "Test symbolic link pointing to outside",
414+
headers: []tar.Header{
415+
{ // Symbolic link to `/etc/passwd`
416+
Typeflag: tar.TypeSymlink,
417+
Name: "foo/passwd",
418+
Linkname: "../../../etc/passwd",
419+
Mode: 0644,
420+
},
421+
},
422+
tag: "symlink",
423+
expectedError: "is outside of",
424+
},
425+
{
426+
name: "Test hard link pointing to outside",
427+
headers: []tar.Header{
428+
{ // Hard link to `/etc/passwd`
429+
Typeflag: tar.TypeLink,
430+
Name: "foo/passwd",
431+
Linkname: "../../../etc/passwd",
432+
Mode: 0644,
433+
},
434+
},
435+
tag: "hardlink",
436+
expectedError: "is outside of",
437+
},
438+
}
439+
for _, test := range tests {
440+
suite.T().Log(test.name)
441+
testVulnerability(test.headers, test.tag, test.expectedError)
442+
}
443+
}
444+
316445
func TestORASTestSuite(t *testing.T) {
317446
suite.Run(t, new(ORASTestSuite))
318447
}

0 commit comments

Comments
 (0)