github.com/hermo/npmi-go@v0.10.1/pkg/archive/tar_test.go (about) 1 package archive 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "compress/gzip" 7 "fmt" 8 "io" 9 "os" 10 "path" 11 "path/filepath" 12 "runtime" 13 "strings" 14 "testing" 15 "time" 16 ) 17 18 func getBaseDir() string { 19 _, filename, _, ok := runtime.Caller(0) 20 if !ok { 21 panic("No caller information") 22 } 23 return path.Dir(filename) 24 } 25 26 func Test_ExtractFilesNormal(t *testing.T) { 27 var buf bytes.Buffer 28 gzw := gzip.NewWriter(&buf) 29 tw := tar.NewWriter(gzw) 30 mDate := time.Date(2021, time.September, 6, 11, 27, 4, 0, time.UTC) 31 zeroDate := time.Time{} 32 33 tarContents := []struct { 34 Name string 35 Content string 36 Date time.Time 37 Mode os.FileMode 38 Type byte 39 }{ 40 {"root.txt", "rootfile", mDate, 0655, tar.TypeReg}, 41 {"somedir", "", mDate, 0700, tar.TypeDir}, 42 {"somedir/sub_link.txt", "sub.txt", zeroDate, 0655, tar.TypeSymlink}, // Link created before actual file on purpose 43 {"somedir/sub.txt", "subfile", mDate, 0644, tar.TypeReg}, 44 {"sub_link.txt", "somedir/sub.txt", zeroDate, 0650, tar.TypeSymlink}, 45 {"somedir/root_link.txt", "../root.txt", zeroDate, 0666, tar.TypeSymlink}, 46 } 47 // Number of files/links expected in archive 48 wantManifestLen := 5 49 50 wantWarnings := 0 51 52 for _, f := range tarContents { 53 data := []byte(f.Content) 54 hdr := tar.Header{ 55 Format: tar.FormatPAX, 56 Typeflag: f.Type, 57 Name: f.Name, 58 ModTime: f.Date, 59 } 60 if f.Type == tar.TypeReg { 61 hdr.Size = int64(len(data)) 62 hdr.Mode = int64(f.Mode) 63 } 64 if f.Type == tar.TypeSymlink { 65 hdr.Linkname = f.Content 66 hdr.Mode = int64(f.Mode & os.ModePerm) 67 } 68 69 if f.Type == tar.TypeDir { 70 hdr.Mode = int64(f.Mode & 0777) 71 } 72 73 err := tw.WriteHeader(&hdr) 74 if err != nil { 75 t.Fatal(err) 76 } 77 if f.Type == tar.TypeReg { 78 _, err := tw.Write(data) 79 if err != nil { 80 t.Fatal(err) 81 } 82 } 83 84 } 85 86 tw.Close() 87 gzw.Close() 88 89 testDir, err := prepareTestDir() 90 if err != nil { 91 t.Fatalf("Can't create temporary test directory: %v", err) 92 } 93 94 defer removeTestDir(testDir) 95 96 err = os.Mkdir("extract", 0700) 97 if err != nil { 98 t.Fatalf("Could not create directory: %v", err) 99 } 100 101 err = os.Chdir("extract") 102 if err != nil { 103 t.Fatalf("Can't chdir to test directory: %v", err) 104 } 105 106 options := TarOptions{ 107 AllowAbsolutePaths: false, 108 AllowDoubleDotPaths: false, 109 AllowLinksOutsideCwd: false, 110 } 111 112 manifest, warnings, err := Extract(&buf, &options) 113 if err != nil { 114 t.Fatalf("Extract failed: %v", err) 115 } 116 117 if len(warnings) != wantWarnings { 118 t.Fatalf("Expected %d warnings, got %d", wantWarnings, len(warnings)) 119 } 120 121 if len(manifest) != wantManifestLen { 122 t.Fatalf("Manifest length=%d,want=%d", len(manifest), wantManifestLen) 123 } 124 125 for _, f := range tarContents { 126 switch f.Type { 127 case tar.TypeDir: 128 fi, err := os.Stat(f.Name) 129 if err != nil { 130 t.Fatal(err) 131 } 132 if fi.IsDir() != true { 133 t.Fatalf("%s should be a directory but is not", f.Name) 134 } 135 if (fi.Mode() & 0777) != f.Mode { 136 t.Fatalf("%s mode=%v, want=%v", f.Name, fi.Mode(), f.Mode) 137 } 138 139 if fi.ModTime().UTC() != f.Date { 140 t.Fatalf("%s mtime=%v, want=%v", f.Name, fi.ModTime().UTC(), f.Date) 141 } 142 case tar.TypeSymlink: 143 li, err := os.Lstat(f.Name) 144 if err != nil { 145 t.Fatal(err) 146 } 147 148 if !f.Date.IsZero() { 149 t.Fatal("Symlink timestamps are not supported and need to be zero") 150 } 151 152 target, err := os.Readlink(f.Name) 153 if err != nil { 154 t.Fatal(err) 155 } 156 if target != f.Content { 157 t.Fatalf("Link %s points to %s, want=%s", li.Name(), f.Content, target) 158 } 159 case tar.TypeReg: 160 fi, err := os.Stat(f.Name) 161 if err != nil { 162 t.Fatal(err) 163 } 164 165 if fi.Mode() != f.Mode { 166 t.Fatalf("%s mode=%v, want=%v", f.Name, fi.Mode(), f.Mode) 167 } 168 169 if fi.ModTime().UTC() != f.Date { 170 t.Fatalf("%s mtime=%v, want=%v", f.Name, fi.ModTime().UTC(), f.Date) 171 } 172 } 173 } 174 } 175 176 func Test_ExtractFilesEvil(t *testing.T) { 177 tests := []struct { 178 Name string 179 Content string 180 Type byte 181 WantedWarnings int 182 }{ 183 {"../evil.txt", "evil", tar.TypeReg, 0}, 184 {"./../evil.txt", "evil", tar.TypeReg, 0}, 185 {"/evil.txt", "evil", tar.TypeReg, 0}, 186 {"C:/Users/Public/evil.txt", "evil", tar.TypeReg, 0}, 187 {"C:|Users/Public/evil.txt", "evil", tar.TypeReg, 0}, 188 {"C:\\Users\\Public\\evil2.txt", "evil2", tar.TypeReg, 0}, 189 {"abs_link", "/etc/passwd", tar.TypeSymlink, 0}, 190 {"outside_link", "../outside_cwd", tar.TypeSymlink, 0}, 191 } 192 193 for _, tt := range tests { 194 t.Run(tt.Name, func(t *testing.T) { 195 testDir, err := prepareTestDir() 196 if err != nil { 197 t.Fatalf("Can't create temporary test directory: %v", err) 198 } 199 200 defer removeTestDir(testDir) 201 202 err = os.Mkdir("extract", 0700) 203 if err != nil { 204 t.Fatalf("Could not create directory: %v", err) 205 } 206 207 err = os.Chdir("extract") 208 if err != nil { 209 t.Fatalf("Can't chdir to test directory: %v", err) 210 } 211 212 var buf bytes.Buffer 213 gzw := gzip.NewWriter(&buf) 214 tw := tar.NewWriter(gzw) 215 216 data := []byte(tt.Content) 217 hdr := tar.Header{ 218 Typeflag: tt.Type, 219 Name: tt.Name, 220 } 221 if tt.Type == tar.TypeReg { 222 hdr.Size = int64(len(data)) 223 } 224 if tt.Type == tar.TypeSymlink { 225 hdr.Linkname = tt.Content 226 } 227 err = tw.WriteHeader(&hdr) 228 if err != nil { 229 t.Fatal(err) 230 } 231 if tt.Type == tar.TypeReg { 232 _, err = tw.Write(data) 233 if err != nil { 234 t.Fatal(err) 235 } 236 } 237 238 tw.Close() 239 gzw.Close() 240 241 options := TarOptions{ 242 AllowAbsolutePaths: false, 243 AllowDoubleDotPaths: false, 244 AllowLinksOutsideCwd: false, 245 } 246 247 _, warnings, err := Extract(&buf, &options) 248 if len(warnings) != tt.WantedWarnings { 249 t.Errorf("Expected %d warnings, got %d", tt.WantedWarnings, len(warnings)) 250 } 251 252 if err == nil { 253 t.Errorf("Extract should have failed but did not: %s", tt.Name) 254 } else { 255 if !strings.Contains(err.Error(), "invalid path") { 256 t.Errorf("Unexpected error: %v", err) 257 } 258 } 259 }) 260 } 261 } 262 263 // Some tests for disallowing bad symlinks 264 func Test_CreateArchiveSymlinks(t *testing.T) { 265 tests := []struct { 266 Source string 267 Target string 268 IsEvil bool 269 WarningCount int 270 }{ 271 // Normal cases 272 {"hello2.txt", "hello.txt", false, 1}, 273 {"hello_subdir_1.txt", "subdir/hello.txt", false, 1}, 274 {"hello_subdir_2.txt", "subdir/.foo/hello.txt", false, 1}, 275 {"subdir/hello_parent_1.txt", "../hello.txt", false, 1}, 276 // Evil cases that are allowed by default for compatibility 🤦♂️ 277 {"abs_1.txt", "/hello.txt", false, 2}, 278 {"abs_2.txt", "/etc/passwd", false, 1}, 279 {"subdir/evil_parent_0.txt", "../../hello.txt", false, 2}, 280 {"evil_parent_1.txt", "../evil.txt", false, 2}, 281 {"evil_parent_2.txt", "./../evil.txt", false, 2}, 282 // Still evil 283 {"evil_abs_win_1.txt", "C:/Users/Public/evil.txt", true, 0}, 284 {"evil_abs_win_2.txt", "C:|Users/Public/evil.txt", true, 0}, 285 {"evil_abs_win_3.txt", "C:\\Users\\Public\\evil2.txt", true, 0}, 286 } 287 288 for _, tt := range tests { 289 var testType string 290 if tt.IsEvil { 291 testType = "EVIL" 292 } else { 293 testType = "NORMAL" 294 } 295 testName := fmt.Sprintf("%s/%s", testType, tt.Source) 296 297 t.Run(testName, func(t *testing.T) { 298 testDir, err := prepareTestDir() 299 if err != nil { 300 t.Fatalf("Can't create temporary test directory: %v", err) 301 } 302 303 defer removeTestDir(testDir) 304 305 err = os.Mkdir("subdir", 0750) 306 if err != nil { 307 t.Fatalf("Can't create test subdir: %v", err) 308 } 309 310 err = os.Symlink(tt.Target, tt.Source) 311 if err != nil { 312 t.Fatalf("Can't create test symlink: %v", err) 313 } 314 315 options := TarOptions{ 316 AllowAbsolutePaths: true, 317 AllowDoubleDotPaths: true, 318 AllowLinksOutsideCwd: true, 319 } 320 warnings, err := Create("temp.tgz", ".", &options) 321 322 if len(warnings) != tt.WarningCount { 323 t.Errorf("Expected %d warnings, got %d", tt.WarningCount, len(warnings)) 324 fmt.Printf("[%s] Warnings: %v\n", testName, warnings) 325 } 326 327 if tt.IsEvil { 328 if err == nil { 329 t.Fatalf("Evil symlink (%s -> %s) should have failed but did not\n", tt.Source, tt.Target) 330 } else { 331 if !strings.Contains(err.Error(), "invalid path") { 332 t.Fatalf("Unexpected error when creating evil symlink (%s -> %s): %v", tt.Source, tt.Target, err) 333 } 334 } 335 } else { 336 if err != nil { 337 if strings.Contains(err.Error(), "invalid path") { 338 t.Fatalf("Normal symlink (%s -> %s) failed: %v", tt.Source, tt.Target, err) 339 } else { 340 t.Fatalf("Unexpected error when creating normal symlink (%s -> %s): %v", tt.Source, tt.Target, err) 341 } 342 } 343 } 344 }) 345 } 346 } 347 348 func Test_CreateArchive_DisallowSymlinksOutsideCwd(t *testing.T) { 349 tests := []struct { 350 Source string 351 Target string 352 IsEvil bool 353 WarningCount int 354 }{ 355 // Normal cases 356 {"hello2.txt", "hello.txt", false, 1}, 357 {"hello_subdir_1.txt", "subdir/hello.txt", false, 1}, 358 {"hello_subdir_2.txt", "subdir/.foo/hello.txt", false, 1}, 359 {"subdir/hello_parent_1.txt", "../hello.txt", false, 1}, 360 // Evil cases 361 {"subdir/evil_parent_0.txt", "../../hello.txt", true, 0}, 362 {"evil_parent_1.txt", "../evil.txt", true, 0}, 363 {"evil_parent_2.txt", "./../evil.txt", true, 0}, 364 {"evil_abs_1.txt", "/evil.txt", true, 0}, 365 {"evil_abs_2.txt", "/etc/passwd", true, 0}, 366 {"evil_abs_win_1.txt", "C:/Users/Public/evil.txt", true, 0}, 367 {"evil_abs_win_2.txt", "C:|Users/Public/evil.txt", true, 0}, 368 {"evil_abs_win_3.txt", "C:\\Users\\Public\\evil2.txt", true, 0}, 369 } 370 371 for _, tt := range tests { 372 var testType string 373 if tt.IsEvil { 374 testType = "EVIL" 375 } else { 376 testType = "NORMAL" 377 } 378 testName := fmt.Sprintf("%s/%s", testType, tt.Source) 379 380 t.Run(testName, func(t *testing.T) { 381 testDir, err := prepareTestDir() 382 if err != nil { 383 t.Fatalf("Can't create temporary test directory: %v", err) 384 } 385 386 defer removeTestDir(testDir) 387 388 err = os.Mkdir("subdir", 0750) 389 if err != nil { 390 t.Fatalf("Can't create test subdir: %v", err) 391 } 392 393 err = os.Symlink(tt.Target, tt.Source) 394 if err != nil { 395 t.Fatalf("Can't create test symlink: %v", err) 396 } 397 398 options := TarOptions{ 399 AllowAbsolutePaths: false, 400 AllowDoubleDotPaths: true, 401 AllowLinksOutsideCwd: false, 402 } 403 warnings, err := Create("temp.tgz", ".", &options) 404 405 if len(warnings) != tt.WarningCount { 406 t.Errorf("Expected %d warnings, got only %d", tt.WarningCount, len(warnings)) 407 } 408 409 if tt.IsEvil { 410 if err == nil { 411 t.Fatalf("Evil symlink (%s -> %s) should have failed but did not\n", tt.Source, tt.Target) 412 } else { 413 if !strings.Contains(err.Error(), "invalid path") { 414 t.Fatalf("Unexpected error when creating evil symlink (%s -> %s): %v", tt.Source, tt.Target, err) 415 } 416 } 417 } else { 418 if err != nil { 419 if strings.Contains(err.Error(), "invalid path") { 420 t.Fatalf("Normal symlink (%s -> %s) failed: %v", tt.Source, tt.Target, err) 421 } else { 422 t.Fatalf("Unexpected error when creating normal symlink (%s -> %s): %v", tt.Source, tt.Target, err) 423 } 424 } 425 } 426 }) 427 } 428 } 429 430 // Create a temporary directory and chdir into it 431 func prepareTestDir() (string, error) { 432 testBaseDir, err := filepath.Abs(fmt.Sprintf("%s/../../testdata", getBaseDir())) 433 if err != nil { 434 return "", fmt.Errorf("can't find test directory: %v", err) 435 } 436 testDir, err := os.MkdirTemp(testBaseDir, "test") 437 if err != nil { 438 return "", fmt.Errorf("can't create temporary test directory: %v", err) 439 } 440 441 err = os.Chdir(testDir) 442 if err != nil { 443 return "", fmt.Errorf("can't chdir to test directory: %v", err) 444 } 445 446 return testDir, nil 447 } 448 449 // Remove test directory after testing 450 func removeTestDir(testDir string) { 451 err := os.Chdir(getBaseDir()) 452 if err != nil { 453 return 454 } 455 err = os.RemoveAll(testDir) 456 if err != nil { 457 fmt.Printf("ERROR: could not remove temp directory: %v", err) 458 } 459 } 460 461 func BenchmarkExtract(b *testing.B) { 462 testDir, err := prepareTestDir() 463 if err != nil { 464 b.Fatalf("Can't create temporary test directory: %v", err) 465 } 466 467 defer removeTestDir(testDir) 468 469 err = os.Mkdir("extract", 0700) 470 if err != nil { 471 b.Fatalf("Could not create directory: %v", err) 472 } 473 474 err = os.Chdir("extract") 475 if err != nil { 476 b.Fatalf("Can't chdir to test directory: %v", err) 477 } 478 testArchive, err := filepath.Abs(fmt.Sprintf("%s/../../bench/extract/test.tgz", getBaseDir())) 479 if err != nil { 480 b.Fatal(err) 481 } 482 483 f, err := os.Open(testArchive) 484 if err != nil { 485 b.Fatal(err) 486 } 487 defer f.Close() 488 489 options := TarOptions{ 490 AllowAbsolutePaths: false, 491 AllowDoubleDotPaths: false, 492 AllowLinksOutsideCwd: false, 493 } 494 495 for i := 0; i < b.N; i++ { 496 f.Seek(0, io.SeekStart) 497 498 _, _, err := Extract(f, &options) 499 if err != nil { 500 b.Fatalf("Extract failed: %v", err) 501 } 502 } 503 } 504 505 func TestBadPath(t *testing.T) { 506 tests := []struct { 507 allowDoubleDot bool 508 allowAbsolutePaths bool 509 Path string 510 Expected bool 511 }{ 512 // Evil inputs, double dots not allowed 513 {false, false, "/evil1.txt", true}, 514 {false, false, `\evil1.txt`, true}, 515 {false, false, "evil11..txt", true}, 516 {false, false, "../evil2.txt", true}, 517 {false, false, "C:/Users/Public/evil3.txt", true}, 518 {false, false, "C:|Users/Public/evil4.txt", true}, 519 {false, false, "<", true}, 520 {false, false, "<foo", true}, 521 {false, false, " <foo2", true}, 522 {false, false, "bar>", true}, 523 {false, false, "win\\separator", true}, 524 {false, false, "\tbadpath", true}, 525 {false, false, "\x01badpath", true}, 526 {false, false, "!badpath", true}, 527 {false, false, "@badpath", true}, 528 {false, false, ":evil.txt", true}, 529 {false, false, ";evil.txt", true}, 530 {false, false, "!evil.txt", true}, 531 {false, false, "@evil.txt", true}, 532 {false, false, "#evil.txt", true}, 533 {false, false, "$evil.txt", true}, 534 {false, false, "%evil.txt", true}, 535 {false, false, "^evil.txt", true}, 536 {false, false, "&evil.txt", true}, 537 {false, false, "*evil.txt", true}, 538 {false, false, "(evil.txt", true}, 539 {false, false, ")evil.txt", true}, 540 {false, false, "+evil.txt", true}, 541 {false, false, "=evil.txt", true}, 542 {false, false, "{evil.txt", true}, 543 {false, false, "}evil.txt", true}, 544 {false, false, "[evil.txt", true}, 545 {false, false, "]evil.txt", true}, 546 {false, false, "|evil.txt", true}, 547 {false, false, "\\evil.txt", true}, 548 {false, false, "/evil.txt", true}, 549 {false, false, "?evil.txt", true}, 550 {false, false, "<evil.txt", true}, 551 {false, false, ">evil.txt", true}, 552 {false, false, ",evil.txt", true}, 553 {false, false, "`evil.txt", true}, 554 {false, false, "~evil.txt", true}, 555 {false, false, " evil.txt", true}, 556 {false, false, "\tevil.txt", true}, 557 {false, false, "\x01evil.txt", true}, 558 {false, false, "\x02evil.txt", true}, 559 {false, false, "\x03evil.txt", true}, 560 {false, false, "\x04evil.txt", true}, 561 {false, false, "\x05evil.txt", true}, 562 {false, false, "\x06evil.txt", true}, 563 {false, false, "\x07evil.txt", true}, 564 {false, false, "\x08evil.txt", true}, 565 {false, false, "\x09evil.txt", true}, 566 {false, false, "\x0aevil.txt", true}, 567 {false, false, "\x0bevil.txt", true}, 568 {false, false, "\x0cevil.txt", true}, 569 {false, false, "\x0devil.txt", true}, 570 {false, false, "\x0eevil.txt", true}, 571 {false, false, "\x0fevil.txt", true}, 572 {false, false, "\x10evil.txt", true}, 573 {false, false, "\x11evil.txt", true}, 574 {false, false, "\x12evil.txt", true}, 575 {false, false, "\x13evil.txt", true}, 576 {false, false, "\x14evil.txt", true}, 577 {false, false, "\x15evil.txt", true}, 578 {false, false, "\x16evil.txt", true}, 579 {false, false, "\x17evil.txt", true}, 580 {false, false, "\x18evil.txt", true}, 581 {false, false, "\x19evil.txt", true}, 582 {false, false, "\x1aevil.txt", true}, 583 {false, false, "\x1bevil.txt", true}, 584 {false, false, "\x1cevil.txt", true}, 585 {false, false, "\x1devil.txt", true}, 586 {false, false, "\x1eevil.txt", true}, 587 {false, false, "\x1fevil.txt", true}, 588 {false, false, " Spaceman", true}, 589 {false, false, "C:\\Users\\Public\\evil5.txt", true}, 590 591 // Evil inputs, double dots allowed 592 {true, false, "/../evil_double_dots_6.txt", true}, 593 {true, false, "/../evil_double_dots_61..txt", true}, 594 595 // Good inputs, double dots disallowed 596 {false, false, "kissa7.txt", false}, 597 {false, false, "foo/bar//double_dots71.txt", false}, 598 {false, false, "Hello dolly", false}, 599 600 // Good inputs, double dots allowed 601 {true, false, "../double_dots8.txt", false}, 602 {true, false, "double_dots9..txt", false}, 603 604 // Good inputs, double dots disallowed, absolute paths allowed 605 {false, true, "/kissa.txt", false}, 606 607 // Evil inputs, double dots disallowed, absolute paths allowed 608 {false, true, "../kissa.txt", true}, 609 {false, true, "/../kissa.txt", true}, 610 611 // Good inputs, double dots allowed, absolute paths allowed 612 {true, true, "/kissa.txt", false}, 613 {true, true, "../kissa.txt", false}, 614 {true, true, "/../kissa.txt", false}, 615 } 616 for _, tt := range tests { 617 t.Run(fmt.Sprintf("allowDoubleDots:%v,Path:%s", tt.allowDoubleDot, tt.Path), func(t *testing.T) { 618 bp := NewBadPath(tt.allowDoubleDot, tt.allowAbsolutePaths) 619 if bp.IsBad(tt.Path) != tt.Expected { 620 t.Errorf("IsBad(%s) did not return %v", tt.Path, tt.Expected) 621 } 622 }) 623 } 624 } 625 626 func BenchmarkBadPath(b *testing.B) { 627 bp := NewBadPath(false, false) 628 path := "node_modules/foo_bar/baz.js/somepath" 629 for i := 0; i < b.N; i++ { 630 bp.IsBad(path) 631 } 632 } 633 634 func BenchmarkCreate(b *testing.B) { 635 testDir, err := prepareTestDir() 636 if err != nil { 637 b.Fatalf("Can't create temporary test directory: %v", err) 638 } 639 640 defer removeTestDir(testDir) 641 642 err = os.Mkdir("compress", 0700) 643 if err != nil { 644 b.Fatalf("Could not create directory: %v", err) 645 } 646 647 err = os.Chdir("compress") 648 if err != nil { 649 b.Fatalf("Can't chdir to test directory: %v", err) 650 } 651 testArchive, err := filepath.Abs(fmt.Sprintf("%s/../../bench/extract/test.tgz", getBaseDir())) 652 if err != nil { 653 b.Fatal(err) 654 } 655 656 f, err := os.Open(testArchive) 657 if err != nil { 658 b.Fatal(err) 659 } 660 options := TarOptions{ 661 AllowAbsolutePaths: false, 662 AllowDoubleDotPaths: false, 663 AllowLinksOutsideCwd: false, 664 } 665 666 _, _, err = Extract(f, &options) 667 if err != nil { 668 b.Fatalf("Extract failed: %v", err) 669 } 670 f.Close() 671 672 for i := 0; i < b.N; i++ { 673 warnings, err := Create("compressed.tgz", "node_modules", &options) 674 if err != nil { 675 b.Fatalf("Create failed: %v", err) 676 } 677 678 if len(warnings) > 100 { 679 b.Errorf("Warnings: %v", warnings) 680 } 681 682 if err = os.Remove("compressed.tgz"); err != nil { 683 b.Fatalf("Remove failed: %v", err) 684 } 685 } 686 }