github.com/oras-project/oras-go@v0.3.0/pkg/oras/oras_test.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package oras 17 18 import ( 19 "archive/tar" 20 "bytes" 21 "compress/gzip" 22 "context" 23 _ "crypto/sha256" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "os" 28 "path/filepath" 29 "testing" 30 "time" 31 32 orascontent "github.com/oras-project/oras-go/pkg/content" 33 34 "github.com/containerd/containerd/images" 35 "github.com/containerd/containerd/remotes" 36 "github.com/containerd/containerd/remotes/docker" 37 "github.com/docker/distribution/configuration" 38 "github.com/docker/distribution/registry" 39 _ "github.com/docker/distribution/registry/storage/driver/inmemory" 40 digest "github.com/opencontainers/go-digest" 41 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 42 "github.com/phayes/freeport" 43 "github.com/stretchr/testify/suite" 44 ) 45 46 var ( 47 testTarball = "../../testdata/charts/chartmuseum-1.8.2.tgz" 48 testDir = "../../testdata/charts/chartmuseum" 49 testDirFiles = []string{ 50 "Chart.yaml", 51 "values.yaml", 52 "README.md", 53 "templates/_helpers.tpl", 54 "templates/NOTES.txt", 55 "templates/service.yaml", 56 ".helmignore", 57 } 58 ) 59 60 type ORASTestSuite struct { 61 suite.Suite 62 DockerRegistryHost string 63 } 64 65 func newContext() context.Context { 66 return context.Background() 67 } 68 69 func newResolver() remotes.Resolver { 70 return docker.NewResolver(docker.ResolverOptions{}) 71 } 72 73 // Start Docker registry 74 func (suite *ORASTestSuite) SetupSuite() { 75 config := &configuration.Configuration{} 76 port, err := freeport.GetFreePort() 77 if err != nil { 78 suite.Nil(err, "no error finding free port for test registry") 79 } 80 suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port) 81 config.HTTP.Addr = fmt.Sprintf(":%d", port) 82 config.HTTP.DrainTimeout = time.Duration(10) * time.Second 83 config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} 84 dockerRegistry, err := registry.NewRegistry(context.Background(), config) 85 suite.Nil(err, "no error finding free port for test registry") 86 87 go dockerRegistry.ListenAndServe() 88 } 89 90 // Push files to docker registry 91 func (suite *ORASTestSuite) Test_0_Push() { 92 var ( 93 err error 94 ref string 95 desc ocispec.Descriptor 96 descriptors []ocispec.Descriptor 97 store *orascontent.FileStore 98 ) 99 100 _, err = Push(newContext(), nil, ref, nil, descriptors) 101 suite.NotNil(err, "error pushing with empty resolver") 102 103 _, err = Push(newContext(), newResolver(), ref, nil, descriptors) 104 suite.NotNil(err, "error pushing when context missing hostname") 105 106 ref = fmt.Sprintf("%s/empty:test", suite.DockerRegistryHost) 107 _, err = Push(newContext(), newResolver(), ref, nil, descriptors) 108 suite.Nil(err, "no error pushing with empty descriptors") 109 110 // Load descriptors with test chart tgz (as single layer) 111 store = orascontent.NewFileStore("") 112 basename := filepath.Base(testTarball) 113 desc, err = store.Add(basename, "", testTarball) 114 suite.Nil(err, "no error loading test chart") 115 descriptors = []ocispec.Descriptor{desc} 116 117 ref = fmt.Sprintf("%s/chart-tgz:test", suite.DockerRegistryHost) 118 _, err = Push(newContext(), newResolver(), ref, store, descriptors) 119 suite.Nil(err, "no error pushing test chart tgz (as single layer)") 120 121 // Load descriptors with test chart dir (each file as layer) 122 testDirAbs, err := filepath.Abs(testDir) 123 suite.Nil(err, "no error parsing test directory") 124 store = orascontent.NewFileStore(testDirAbs) 125 descriptors = []ocispec.Descriptor{} 126 var ff = func(pathX string, infoX os.FileInfo, errX error) error { 127 if !infoX.IsDir() { 128 filename := filepath.Join(filepath.Dir(pathX), infoX.Name()) 129 name := filepath.ToSlash(filename) 130 desc, err = store.Add(name, "", filename) 131 if err != nil { 132 return err 133 } 134 descriptors = append(descriptors, desc) 135 } 136 return nil 137 } 138 139 cwd, _ := os.Getwd() 140 os.Chdir(testDir) 141 filepath.Walk(".", ff) 142 os.Chdir(cwd) 143 144 ref = fmt.Sprintf("%s/chart-dir:test", suite.DockerRegistryHost) 145 _, err = Push(newContext(), newResolver(), ref, store, descriptors) 146 suite.Nil(err, "no error pushing test chart dir (each file as layer)") 147 } 148 149 // Pull files and verify descriptors 150 func (suite *ORASTestSuite) Test_1_Pull() { 151 var ( 152 err error 153 ref string 154 descriptors []ocispec.Descriptor 155 store *orascontent.Memorystore 156 ) 157 158 _, descriptors, err = Pull(newContext(), nil, ref, nil) 159 suite.NotNil(err, "error pulling with empty resolver") 160 suite.Nil(descriptors, "descriptors nil pulling with empty resolver") 161 162 // Pull non-existant 163 store = orascontent.NewMemoryStore() 164 ref = fmt.Sprintf("%s/nonexistant:test", suite.DockerRegistryHost) 165 _, descriptors, err = Pull(newContext(), newResolver(), ref, store) 166 suite.NotNil(err, "error pulling non-existant ref") 167 suite.Nil(descriptors, "descriptors empty with error") 168 169 // Pull chart-tgz 170 store = orascontent.NewMemoryStore() 171 ref = fmt.Sprintf("%s/chart-tgz:test", suite.DockerRegistryHost) 172 _, descriptors, err = Pull(newContext(), newResolver(), ref, store) 173 suite.Nil(err, "no error pulling chart-tgz ref") 174 175 // Verify the descriptors, single layer/file 176 content, err := ioutil.ReadFile(testTarball) 177 suite.Nil(err, "no error loading test chart") 178 name := filepath.Base(testTarball) 179 _, actualContent, ok := store.GetByName(name) 180 suite.True(ok, "find in memory") 181 suite.Equal(content, actualContent, ".tgz content matches on pull") 182 183 // Pull chart-dir 184 store = orascontent.NewMemoryStore() 185 ref = fmt.Sprintf("%s/chart-dir:test", suite.DockerRegistryHost) 186 _, descriptors, err = Pull(newContext(), newResolver(), ref, store) 187 suite.Nil(err, "no error pulling chart-dir ref") 188 189 // Verify the descriptors, multiple layers/files 190 cwd, _ := os.Getwd() 191 os.Chdir(testDir) 192 for _, filename := range testDirFiles { 193 content, err = ioutil.ReadFile(filename) 194 suite.Nil(err, fmt.Sprintf("no error loading %s", filename)) 195 _, actualContent, ok := store.GetByName(filename) 196 suite.True(ok, "find in memory") 197 suite.Equal(content, actualContent, fmt.Sprintf("%s content matches on pull", filename)) 198 } 199 os.Chdir(cwd) 200 } 201 202 // Push and pull with customized media types 203 func (suite *ORASTestSuite) Test_2_MediaType() { 204 var ( 205 testData = [][]string{ 206 {"hi.txt", "application/vnd.me.hi", "hi"}, 207 {"bye.txt", "application/vnd.me.bye", "bye"}, 208 } 209 err error 210 ref string 211 descriptors []ocispec.Descriptor 212 store *orascontent.Memorystore 213 ) 214 215 // Push content with customized media types 216 store = orascontent.NewMemoryStore() 217 descriptors = nil 218 for _, data := range testData { 219 desc := store.Add(data[0], data[1], []byte(data[2])) 220 descriptors = append(descriptors, desc) 221 } 222 ref = fmt.Sprintf("%s/media-type:test", suite.DockerRegistryHost) 223 _, err = Push(newContext(), newResolver(), ref, store, descriptors) 224 suite.Nil(err, "no error pushing test data with customized media type") 225 226 // Pull with all media types 227 store = orascontent.NewMemoryStore() 228 ref = fmt.Sprintf("%s/media-type:test", suite.DockerRegistryHost) 229 _, descriptors, err = Pull(newContext(), newResolver(), ref, store) 230 suite.Nil(err, "no error pulling media-type ref") 231 suite.Equal(2, len(descriptors), "number of contents matches on pull") 232 for _, data := range testData { 233 _, actualContent, ok := store.GetByName(data[0]) 234 suite.True(ok, "find in memory") 235 content := []byte(data[2]) 236 suite.Equal(content, actualContent, "test content matches on pull") 237 } 238 239 // Pull with specified media type 240 store = orascontent.NewMemoryStore() 241 ref = fmt.Sprintf("%s/media-type:test", suite.DockerRegistryHost) 242 _, descriptors, err = Pull(newContext(), newResolver(), ref, store, WithAllowedMediaType(testData[0][1])) 243 suite.Nil(err, "no error pulling media-type ref") 244 suite.Equal(1, len(descriptors), "number of contents matches on pull") 245 for _, data := range testData[:1] { 246 _, actualContent, ok := store.GetByName(data[0]) 247 suite.True(ok, "find in memory") 248 content := []byte(data[2]) 249 suite.Equal(content, actualContent, "test content matches on pull") 250 } 251 252 // Pull with non-existing media type 253 store = orascontent.NewMemoryStore() 254 ref = fmt.Sprintf("%s/media-type:test", suite.DockerRegistryHost) 255 _, descriptors, err = Pull(newContext(), newResolver(), ref, store, WithAllowedMediaType("non.existing.media.type")) 256 suite.Nil(err, "no error pulling media-type ref") 257 suite.Equal(0, len(descriptors), "number of contents matches on pull") 258 } 259 260 // Pull with condition 261 func (suite *ORASTestSuite) Test_3_Conditional_Pull() { 262 var ( 263 testData = [][]string{ 264 {"version.txt", "edge"}, 265 {"content.txt", "hello world"}, 266 } 267 err error 268 ref string 269 descriptors []ocispec.Descriptor 270 store *orascontent.Memorystore 271 stop bool 272 ) 273 274 // Push test content 275 store = orascontent.NewMemoryStore() 276 descriptors = nil 277 for _, data := range testData { 278 desc := store.Add(data[0], "", []byte(data[1])) 279 descriptors = append(descriptors, desc) 280 } 281 ref = fmt.Sprintf("%s/conditional-pull:test", suite.DockerRegistryHost) 282 _, err = Push(newContext(), newResolver(), ref, store, descriptors) 283 suite.Nil(err, "no error pushing test data") 284 285 // Pull all contents in sequence 286 store = orascontent.NewMemoryStore() 287 ref = fmt.Sprintf("%s/conditional-pull:test", suite.DockerRegistryHost) 288 _, descriptors, err = Pull(newContext(), newResolver(), ref, store, WithPullByBFS) 289 suite.Nil(err, "no error pulling ref") 290 suite.Equal(2, len(descriptors), "number of contents matches on pull") 291 for i, data := range testData { 292 _, actualContent, ok := store.GetByName(data[0]) 293 suite.True(ok, "find in memory") 294 content := []byte(data[1]) 295 suite.Equal(content, actualContent, "test content matches on pull") 296 name, _ := orascontent.ResolveName(descriptors[i]) 297 suite.Equal(data[0], name, "content sequence matches on pull") 298 } 299 300 // Selective pull contents: stop at the very beginning 301 store = orascontent.NewMemoryStore() 302 ref = fmt.Sprintf("%s/conditional-pull:test", suite.DockerRegistryHost) 303 _, descriptors, err = Pull(newContext(), newResolver(), ref, store, WithPullByBFS, 304 WithPullBaseHandler(images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 305 if name, ok := orascontent.ResolveName(desc); ok && name == testData[0][0] { 306 return nil, ErrStopProcessing 307 } 308 return nil, nil 309 }))) 310 suite.Nil(err, "no error pulling ref") 311 suite.Equal(0, len(descriptors), "number of contents matches on pull") 312 313 // Selective pull contents: stop in the middle 314 store = orascontent.NewMemoryStore() 315 ref = fmt.Sprintf("%s/conditional-pull:test", suite.DockerRegistryHost) 316 stop = false 317 _, descriptors, err = Pull(newContext(), newResolver(), ref, store, WithPullByBFS, 318 WithPullBaseHandler(images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 319 if stop { 320 return nil, ErrStopProcessing 321 } 322 if name, ok := orascontent.ResolveName(desc); ok && name == testData[0][0] { 323 stop = true 324 } 325 return nil, nil 326 }))) 327 suite.Nil(err, "no error pulling ref") 328 suite.Equal(1, len(descriptors), "number of contents matches on pull") 329 for _, data := range testData[:1] { 330 _, actualContent, ok := store.GetByName(data[0]) 331 suite.True(ok, "find in memory") 332 content := []byte(data[1]) 333 suite.Equal(content, actualContent, "test content matches on pull") 334 } 335 } 336 337 // Test for vulnerability GHSA-g5v4-5x39-vwhx 338 func (suite *ORASTestSuite) Test_4_GHSA_g5v4_5x39_vwhx() { 339 var testVulnerability = func(headers []tar.Header, tag string, expectedError string) { 340 // Step 1: build malicious tar+gzip 341 buf := bytes.NewBuffer(nil) 342 digester := digest.Canonical.Digester() 343 zw := gzip.NewWriter(io.MultiWriter(buf, digester.Hash())) 344 tarDigester := digest.Canonical.Digester() 345 tw := tar.NewWriter(io.MultiWriter(zw, tarDigester.Hash())) 346 for _, header := range headers { 347 err := tw.WriteHeader(&header) 348 suite.Nil(err, "error writing header") 349 } 350 err := tw.Close() 351 suite.Nil(err, "error closing tar") 352 err = zw.Close() 353 suite.Nil(err, "error closing gzip") 354 355 // Step 2: construct malicious descriptor 356 evilDesc := ocispec.Descriptor{ 357 MediaType: ocispec.MediaTypeImageLayerGzip, 358 Digest: digester.Digest(), 359 Size: int64(buf.Len()), 360 Annotations: map[string]string{ 361 orascontent.AnnotationDigest: tarDigester.Digest().String(), 362 orascontent.AnnotationUnpack: "true", 363 ocispec.AnnotationTitle: "foo", 364 }, 365 } 366 367 // Step 3: upload malicious artifact to registry 368 memoryStore := orascontent.NewMemoryStore() 369 memoryStore.Set(evilDesc, buf.Bytes()) 370 ref := fmt.Sprintf("%s/evil:%s", suite.DockerRegistryHost, tag) 371 _, err = Push(newContext(), newResolver(), ref, memoryStore, []ocispec.Descriptor{evilDesc}) 372 suite.Nil(err, "no error pushing test data") 373 374 // Step 4: pull malicious tar with oras filestore and ensure error 375 tempDir, err := ioutil.TempDir("", "oras_test") 376 if err != nil { 377 suite.FailNow("error creating temp directory", err) 378 } 379 defer os.RemoveAll(tempDir) 380 store := orascontent.NewFileStore(tempDir) 381 defer store.Close() 382 ref = fmt.Sprintf("%s/evil:%s", suite.DockerRegistryHost, tag) 383 _, _, err = Pull(newContext(), newResolver(), ref, store) 384 suite.NotNil(err, "error expected pulling malicious tar") 385 suite.Contains(err.Error(), 386 expectedError, 387 "did not get correct error message", 388 ) 389 } 390 391 tests := []struct { 392 name string 393 headers []tar.Header 394 tag string 395 expectedError string 396 }{ 397 { 398 name: "Test symbolic link path traversal", 399 headers: []tar.Header{ 400 { 401 Typeflag: tar.TypeDir, 402 Name: "foo/subdir/", 403 Mode: 0755, 404 }, 405 { // Symbolic link to `foo` 406 Typeflag: tar.TypeSymlink, 407 Name: "foo/subdir/parent", 408 Linkname: "..", 409 Mode: 0755, 410 }, 411 { // Symbolic link to `../etc/passwd` 412 Typeflag: tar.TypeSymlink, 413 Name: "foo/subdir/parent/passwd", 414 Linkname: "../../etc/passwd", 415 Mode: 0644, 416 }, 417 { // Symbolic link to `../etc` 418 Typeflag: tar.TypeSymlink, 419 Name: "foo/subdir/parent/etc", 420 Linkname: "../../etc", 421 Mode: 0644, 422 }, 423 }, 424 tag: "symlink_path", 425 expectedError: "no symbolic link allowed", 426 }, 427 { 428 name: "Test symbolic link pointing to outside", 429 headers: []tar.Header{ 430 { // Symbolic link to `/etc/passwd` 431 Typeflag: tar.TypeSymlink, 432 Name: "foo/passwd", 433 Linkname: "../../../etc/passwd", 434 Mode: 0644, 435 }, 436 }, 437 tag: "symlink", 438 expectedError: "is outside of", 439 }, 440 { 441 name: "Test hard link pointing to outside", 442 headers: []tar.Header{ 443 { // Hard link to `/etc/passwd` 444 Typeflag: tar.TypeLink, 445 Name: "foo/passwd", 446 Linkname: "../../../etc/passwd", 447 Mode: 0644, 448 }, 449 }, 450 tag: "hardlink", 451 expectedError: "is outside of", 452 }, 453 } 454 for _, test := range tests { 455 suite.T().Log(test.name) 456 testVulnerability(test.headers, test.tag, test.expectedError) 457 } 458 } 459 460 func TestORASTestSuite(t *testing.T) { 461 suite.Run(t, new(ORASTestSuite)) 462 }