diff --git a/knotserver/git/fork.go b/knotserver/git/fork.go index 2996e43b..402d09b6 100644 --- a/knotserver/git/fork.go +++ b/knotserver/git/fork.go @@ -23,16 +23,31 @@ func Fork(repoPath, source string, cfg *knotconfig.Config) error { // post-clone configure step in sb. The initial clone itself is not sandboxed // because the target directory doesn't exist yet when the ruleset is applied. func ForkWithSandbox(repoPath, source string, cfg *knotconfig.Config, sb sandbox.Backend) error { + if !(source == "" || source[0] != '-') { + return fmt.Errorf("invalid source: %q", source) + } u, err := url.Parse(source) if err != nil { return fmt.Errorf("failed to parse source URL: %w", err) } + if u.Scheme != "https" && u.Scheme != "http" { + return fmt.Errorf("invalid scheme: %q", u.Scheme) + } + if u.Host == "" { + return fmt.Errorf("missing host: %q", source) + } if o := optimizeClone(u, cfg); o != nil { u = o } - cloneCmd := exec.Command("git", "clone", "--bare", u.String(), repoPath) + cloneCmd := exec.Command( + "git", + "-c", "protocol.ext.allow=never", + "clone", "--bare", u.String(), repoPath, + ) + cloneCmd.Env = append(cloneCmd.Env, "GIT_PROTOCOL_FROM_USER=0") + cloneCmd.Env = append(cloneCmd.Env, "GIT_TERMINAL_PROMPT=0") if err := cloneCmd.Run(); err != nil { return fmt.Errorf("failed to bare clone repository: %w", err) } diff --git a/knotserver/git/fork_test.go b/knotserver/git/fork_test.go new file mode 100644 index 00000000..613255f8 --- /dev/null +++ b/knotserver/git/fork_test.go @@ -0,0 +1,33 @@ +package git + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + knotconfig "tangled.org/core/knotserver/config" +) + +func TestForkRejectsUnsafeSources(t *testing.T) { + t.Parallel() + + for _, source := range []string{ + "", + "--upload-pack=touch /tmp/pwned", + "-u/bin/sh", + "ext::sh -c touch% /tmp/pwned", + "file:///etc/passwd", + "git://example.com/repo", + "ssh://example.com/repo", + "/etc/passwd", + } { + t.Run(source, func(t *testing.T) { + repoPath := filepath.Join(t.TempDir(), "fork") + err := Fork(repoPath, source, &knotconfig.Config{}) + assert.Error(t, err, "source %q should be rejected", source) + _, statErr := os.Stat(repoPath) + assert.True(t, os.IsNotExist(statErr), "source %q must not create a repo", source) + }) + } +} diff --git a/localinfra/knot.Dockerfile b/localinfra/knot.Dockerfile index aab62bf5..3d3b1331 100644 --- a/localinfra/knot.Dockerfile +++ b/localinfra/knot.Dockerfile @@ -96,5 +96,7 @@ RUN chmod +x /usr/local/bin/knot-entrypoint.sh VOLUME /home/git EXPOSE 22 5555 +WORKDIR /home/git + ENTRYPOINT ["/sbin/tini", "--"] CMD ["/usr/local/bin/knot-entrypoint.sh"]