|
9 | 9 | "fmt" |
10 | 10 | "os" |
11 | 11 | "path/filepath" |
| 12 | + "strings" |
12 | 13 | "testing" |
13 | 14 |
|
14 | 15 | "github.com/hashicorp/go-version" |
@@ -2582,3 +2583,139 @@ func TestDefaultProjectCommandRunner_ApprovePolicies_HashAwareApproval(t *testin |
2582 | 2583 | }) |
2583 | 2584 | } |
2584 | 2585 | } |
| 2586 | + |
| 2587 | +func TestDefaultProjectCommandRunner_PathTraversal(t *testing.T) { |
| 2588 | + const defaultTraversalPattern = "../../../../etc" |
| 2589 | + |
| 2590 | + cases := []struct { |
| 2591 | + name string |
| 2592 | + traversalPatterns []string |
| 2593 | + expectUnlock bool |
| 2594 | + runFn func(runner *events.DefaultProjectCommandRunner, ctx command.ProjectContext) error |
| 2595 | + setupWorkingDir func(mockWorkingDir *mocks.MockWorkingDir, repoDir string) |
| 2596 | + }{ |
| 2597 | + { |
| 2598 | + name: "Plan", |
| 2599 | + traversalPatterns: []string{ |
| 2600 | + "../../../../etc", |
| 2601 | + "../etc", |
| 2602 | + "sub/../../etc", |
| 2603 | + }, |
| 2604 | + expectUnlock: true, |
| 2605 | + setupWorkingDir: func(mockWorkingDir *mocks.MockWorkingDir, repoDir string) { |
| 2606 | + When(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[string]())). |
| 2607 | + ThenReturn(repoDir, nil) |
| 2608 | + When(mockWorkingDir.MergeAgain(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[string]())). |
| 2609 | + ThenReturn(false, nil) |
| 2610 | + }, |
| 2611 | + runFn: func(runner *events.DefaultProjectCommandRunner, ctx command.ProjectContext) error { |
| 2612 | + return runner.Plan(ctx).Error |
| 2613 | + }, |
| 2614 | + }, |
| 2615 | + { |
| 2616 | + name: "Apply", |
| 2617 | + traversalPatterns: []string{defaultTraversalPattern}, |
| 2618 | + setupWorkingDir: func(mockWorkingDir *mocks.MockWorkingDir, repoDir string) { |
| 2619 | + When(mockWorkingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())). |
| 2620 | + ThenReturn(repoDir, nil) |
| 2621 | + }, |
| 2622 | + runFn: func(runner *events.DefaultProjectCommandRunner, ctx command.ProjectContext) error { |
| 2623 | + return runner.Apply(ctx).Error |
| 2624 | + }, |
| 2625 | + }, |
| 2626 | + { |
| 2627 | + name: "PolicyCheck", |
| 2628 | + traversalPatterns: []string{defaultTraversalPattern}, |
| 2629 | + expectUnlock: true, |
| 2630 | + setupWorkingDir: func(mockWorkingDir *mocks.MockWorkingDir, repoDir string) { |
| 2631 | + When(mockWorkingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())). |
| 2632 | + ThenReturn(repoDir, nil) |
| 2633 | + }, |
| 2634 | + runFn: func(runner *events.DefaultProjectCommandRunner, ctx command.ProjectContext) error { |
| 2635 | + return runner.PolicyCheck(ctx).Error |
| 2636 | + }, |
| 2637 | + }, |
| 2638 | + { |
| 2639 | + name: "Version", |
| 2640 | + traversalPatterns: []string{defaultTraversalPattern}, |
| 2641 | + setupWorkingDir: func(mockWorkingDir *mocks.MockWorkingDir, repoDir string) { |
| 2642 | + When(mockWorkingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())). |
| 2643 | + ThenReturn(repoDir, nil) |
| 2644 | + }, |
| 2645 | + runFn: func(runner *events.DefaultProjectCommandRunner, ctx command.ProjectContext) error { |
| 2646 | + return runner.Version(ctx).Error |
| 2647 | + }, |
| 2648 | + }, |
| 2649 | + { |
| 2650 | + name: "Import", |
| 2651 | + traversalPatterns: []string{defaultTraversalPattern}, |
| 2652 | + setupWorkingDir: func(mockWorkingDir *mocks.MockWorkingDir, repoDir string) { |
| 2653 | + When(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[string]())). |
| 2654 | + ThenReturn(repoDir, nil) |
| 2655 | + }, |
| 2656 | + runFn: func(runner *events.DefaultProjectCommandRunner, ctx command.ProjectContext) error { |
| 2657 | + return runner.Import(ctx).Error |
| 2658 | + }, |
| 2659 | + }, |
| 2660 | + { |
| 2661 | + name: "StateRm", |
| 2662 | + traversalPatterns: []string{defaultTraversalPattern}, |
| 2663 | + setupWorkingDir: func(mockWorkingDir *mocks.MockWorkingDir, repoDir string) { |
| 2664 | + When(mockWorkingDir.Clone(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Any[string]())). |
| 2665 | + ThenReturn(repoDir, nil) |
| 2666 | + }, |
| 2667 | + runFn: func(runner *events.DefaultProjectCommandRunner, ctx command.ProjectContext) error { |
| 2668 | + return runner.StateRm(ctx).Error |
| 2669 | + }, |
| 2670 | + }, |
| 2671 | + } |
| 2672 | + |
| 2673 | + for _, tc := range cases { |
| 2674 | + for _, pattern := range tc.traversalPatterns { |
| 2675 | + t.Run(tc.name+" rejects traversal pattern "+pattern, func(t *testing.T) { |
| 2676 | + RegisterMockTestingT(t) |
| 2677 | + mockWorkingDir := mocks.NewMockWorkingDir() |
| 2678 | + mockLocker := mocks.NewMockProjectLocker() |
| 2679 | + runner := &events.DefaultProjectCommandRunner{ |
| 2680 | + Locker: mockLocker, |
| 2681 | + LockURLGenerator: mockURLGenerator{}, |
| 2682 | + WorkingDir: mockWorkingDir, |
| 2683 | + WorkingDirLocker: events.NewDefaultWorkingDirLocker(), |
| 2684 | + } |
| 2685 | + repoDir := t.TempDir() |
| 2686 | + tc.setupWorkingDir(mockWorkingDir, repoDir) |
| 2687 | + unlockCalled := false |
| 2688 | + When(mockLocker.TryLock( |
| 2689 | + Any[logging.SimpleLogging](), |
| 2690 | + Any[models.PullRequest](), |
| 2691 | + Any[models.User](), |
| 2692 | + Any[string](), |
| 2693 | + Any[models.Project](), |
| 2694 | + AnyBool(), |
| 2695 | + )).ThenReturn(&events.TryLockResponse{ |
| 2696 | + LockAcquired: true, |
| 2697 | + LockKey: "lock-key", |
| 2698 | + UnlockFn: func() error { |
| 2699 | + unlockCalled = true |
| 2700 | + return nil |
| 2701 | + }, |
| 2702 | + }, nil) |
| 2703 | + ctx := command.ProjectContext{ |
| 2704 | + Log: logging.NewNoopLogger(t), |
| 2705 | + Workspace: "default", |
| 2706 | + RepoRelDir: pattern, |
| 2707 | + RePlanCmd: "atlantis plan -d .", |
| 2708 | + } |
| 2709 | + err := tc.runFn(runner, ctx) |
| 2710 | + Assert(t, err != nil, "expected error for RepoRelDir %q in runner %q", pattern, tc.name) |
| 2711 | + Assert(t, |
| 2712 | + strings.Contains(err.Error(), "project path traversal detected"), |
| 2713 | + "expected traversal error for runner %q with RepoRelDir %q, got: %s", tc.name, pattern, err, |
| 2714 | + ) |
| 2715 | + if tc.expectUnlock { |
| 2716 | + Assert(t, unlockCalled, "expected runner %q with RepoRelDir %q to release project lock", tc.name, pattern) |
| 2717 | + } |
| 2718 | + }) |
| 2719 | + } |
| 2720 | + } |
| 2721 | +} |
0 commit comments