Skip to content

Commit bed9fb1

Browse files
authored
Prevent git commands in dangerous dirs (#222)
* Prevent git commands in dangerous dirs * Try E2E * Go fix * Fix vendoring and lint * Tweak message * lint * Tweak message * Fix fmt * Add escape hatch
1 parent cacdd02 commit bed9fb1

File tree

12 files changed

+3711
-10
lines changed

12 files changed

+3711
-10
lines changed

e2e/bitrise.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,26 @@ workflows:
502502
- _check_outputs
503503
- _teardown
504504

505+
test_dangerous_clone_dir:
506+
steps:
507+
- script:
508+
inputs:
509+
- content: |-
510+
#/bin/env bash
511+
set -x
512+
bitrise run --config=./e2e/bitrise.yml utility_dangerous_clone_dir
513+
if [ $? == 0 ]; then
514+
exit 1
515+
fi
516+
517+
utility_dangerous_clone_dir:
518+
envs:
519+
- TEST_REPO_URL: https://github.com/websitebot/git-repo-fixture-private.git
520+
- BRANCH: main
521+
- WORKDIR: $HOME
522+
after_run:
523+
- _run
524+
505525
_check_changelog:
506526
steps:
507527
- generate-changelog:

main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/bitrise-io/go-utils/v2/errorutil"
1111
. "github.com/bitrise-io/go-utils/v2/exitcode"
1212
"github.com/bitrise-io/go-utils/v2/log"
13+
"github.com/bitrise-io/go-utils/v2/pathutil"
1314
"github.com/bitrise-steplib/steps-git-clone/gitclone/tracker"
1415
"github.com/bitrise-steplib/steps-git-clone/step"
1516
)
@@ -53,6 +54,7 @@ func createStep(logger log.Logger) step.GitCloneStep {
5354
tracker := tracker.NewStepTracker(envRepo, logger)
5455
inputParser := stepconf.NewInputParser(envRepo)
5556
cmdFactory := command.NewFactory(envRepo)
57+
pathModififer := pathutil.NewPathModifier()
5658

57-
return step.NewGitCloneStep(logger, tracker, inputParser, cmdFactory)
59+
return step.NewGitCloneStep(logger, tracker, inputParser, envRepo, cmdFactory, pathModififer)
5860
}

step/step.go

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import (
66
"github.com/bitrise-io/go-steputils/v2/stepconf"
77
"github.com/bitrise-io/go-utils/retry"
88
"github.com/bitrise-io/go-utils/v2/command"
9+
"github.com/bitrise-io/go-utils/v2/env"
910
"github.com/bitrise-io/go-utils/v2/log"
11+
"github.com/bitrise-io/go-utils/v2/log/colorstring"
12+
"github.com/bitrise-io/go-utils/v2/pathutil"
1013
"github.com/bitrise-steplib/steps-git-clone/gitclone"
1114
"github.com/bitrise-steplib/steps-git-clone/gitclone/bitriseapi"
1215
"github.com/bitrise-steplib/steps-git-clone/gitclone/tracker"
@@ -47,18 +50,22 @@ type Config struct {
4750
}
4851

4952
type GitCloneStep struct {
50-
logger log.Logger
51-
tracker tracker.StepTracker
52-
inputParser stepconf.InputParser
53-
cmdFactory command.Factory
53+
logger log.Logger
54+
tracker tracker.StepTracker
55+
inputParser stepconf.InputParser
56+
envRepo env.Repository
57+
cmdFactory command.Factory
58+
pathModifier pathutil.PathModifier
5459
}
5560

56-
func NewGitCloneStep(logger log.Logger, tracker tracker.StepTracker, inputParser stepconf.InputParser, cmdFactory command.Factory) GitCloneStep {
61+
func NewGitCloneStep(logger log.Logger, tracker tracker.StepTracker, inputParser stepconf.InputParser, envRepo env.Repository, cmdFactory command.Factory, pathModifier pathutil.PathModifier) GitCloneStep {
5762
return GitCloneStep{
58-
logger: logger,
59-
tracker: tracker,
60-
inputParser: inputParser,
61-
cmdFactory: cmdFactory,
63+
logger: logger,
64+
tracker: tracker,
65+
inputParser: inputParser,
66+
envRepo: envRepo,
67+
cmdFactory: cmdFactory,
68+
pathModifier: pathModifier,
6269
}
6370
}
6471

@@ -69,6 +76,20 @@ func (g GitCloneStep) ProcessConfig() (Config, error) {
6976
}
7077
stepconf.Print(input)
7178

79+
if g.isCloneDirDangerous(input.CloneIntoDir) && g.envRepo.Get("BITRISE_GIT_CLONE_FORCE_RUN") != "true" {
80+
g.logger.Println()
81+
g.logger.Println()
82+
g.logger.Errorf("BEWARE: The git clone directory is set to %s", input.CloneIntoDir)
83+
g.logger.Errorf("This is probably not what you want, as the step could overwrite files in the directory.")
84+
g.logger.Printf("To update the path, you have a few options:")
85+
g.logger.Printf("1. Change the %s step input", colorstring.Cyan("clone_into_dir"))
86+
g.logger.Printf("2. If not specified, %s defaults to %s. Check the value of this env var.", colorstring.Cyan("clone_into_dir"), colorstring.Cyan("$BITRISE_SOURCE_DIR"))
87+
g.logger.Printf("3. When using self-hosted agents, you can customize %s and other important values in the %s file.", colorstring.Cyan("$BITRISE_SOURCE_DIR"), colorstring.Cyan("~/.bitrise/agent-config.yml"))
88+
g.logger.Printf("If you are sure you want to proceed, you can set the %s env var to force the step to run.", colorstring.Cyan("BITRISE_GIT_CLONE_FORCE_RUN=true"))
89+
90+
return Config{}, fmt.Errorf("dangerous clone directory detected")
91+
}
92+
7293
return Config{input}, nil
7394
}
7495

@@ -99,6 +120,46 @@ func (g GitCloneStep) ExportOutputs(runResult gitclone.CheckoutStateResult) erro
99120
return nil
100121
}
101122

123+
func (g GitCloneStep) isCloneDirDangerous(path string) bool {
124+
blocklist := []string{
125+
"~",
126+
"~/Downloads",
127+
"~/Documents",
128+
"~/Desktop",
129+
"/bin",
130+
"/usr/bin",
131+
"/etc",
132+
"/Applications",
133+
"/Library",
134+
"~/Library",
135+
"~/.config",
136+
"~/.bitrise",
137+
"~/.ssh",
138+
}
139+
140+
absClonePath, err := g.pathModifier.AbsPath(path)
141+
if err != nil {
142+
g.logger.Warnf("Failed to get absolute path of clone directory: %s", err)
143+
// The path could be incorrect for many reasons, but we don't want to cause a false positive.
144+
// A true positive will be caught by the git command anyway.
145+
return false
146+
}
147+
148+
for _, dangerousPath := range blocklist {
149+
absDangerousPath, err := g.pathModifier.AbsPath(dangerousPath)
150+
if err != nil {
151+
// Not all blocklisted paths are valid on all systems, so we ignore this error.
152+
continue
153+
}
154+
155+
if absClonePath == absDangerousPath {
156+
return true
157+
}
158+
}
159+
160+
return false
161+
}
162+
102163
func convertConfig(config Config) gitclone.Config {
103164
return gitclone.Config{
104165
ShouldMergePR: config.ShouldMergePR,

step/step_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package step
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/bitrise-io/go-steputils/v2/stepconf"
9+
"github.com/bitrise-io/go-utils/v2/command"
10+
"github.com/bitrise-io/go-utils/v2/env"
11+
"github.com/bitrise-io/go-utils/v2/log"
12+
"github.com/bitrise-io/go-utils/v2/pathutil"
13+
"github.com/bitrise-steplib/steps-git-clone/gitclone/tracker"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func Test_GitCloneStep_IsCloneDirDangerous(t *testing.T) {
18+
home, err := os.UserHomeDir()
19+
require.NoError(t, err)
20+
21+
tests := []struct {
22+
name string
23+
path string
24+
expected bool
25+
}{
26+
{
27+
name: "Safe path in temp dir",
28+
path: "$TMPDIR/clone",
29+
expected: false,
30+
},
31+
{
32+
name: "Safe path in home",
33+
path: filepath.Join(home, "clone"),
34+
expected: false,
35+
},
36+
{
37+
name: "Home as env var",
38+
path: "$HOME",
39+
expected: true,
40+
},
41+
{
42+
name: "Home as tilde",
43+
path: "~",
44+
expected: true,
45+
},
46+
{
47+
name: "Home as absolute path",
48+
path: home,
49+
expected: true,
50+
},
51+
{
52+
name: "Dangerous path with tilde",
53+
path: "~/Documents",
54+
expected: true,
55+
},
56+
{
57+
name: "Dangerous absolute path",
58+
path: filepath.Join(home, ".ssh"),
59+
expected: true,
60+
},
61+
{
62+
name: "Nonexistent env var only",
63+
path: "$NONEXISTENT",
64+
expected: false,
65+
},
66+
}
67+
68+
logger := log.NewLogger()
69+
envRepo := env.NewRepository()
70+
tracker := tracker.NewStepTracker(envRepo, logger)
71+
inputParser := stepconf.NewInputParser(envRepo)
72+
cmdFactory := command.NewFactory(envRepo)
73+
pathModifier := pathutil.NewPathModifier()
74+
75+
gitCloneStep := NewGitCloneStep(logger, tracker, inputParser, envRepo, cmdFactory, pathModifier)
76+
77+
for _, test := range tests {
78+
t.Run(test.name, func(t *testing.T) {
79+
result := gitCloneStep.isCloneDirDangerous(test.path)
80+
require.Equal(t, test.expected, result)
81+
})
82+
}
83+
}

vendor/github.com/stretchr/testify/require/doc.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/stretchr/testify/require/forward_requirements.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)