Skip to content

Commit 38256df

Browse files
committed
fix/batches: prevent repository archives from enabling Git hooks
1 parent c4030b5 commit 38256df

4 files changed

Lines changed: 106 additions & 0 deletions

File tree

internal/batches/workspace/bind_workspace.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,15 @@ func unzip(ctx context.Context, zipFile, dest string) error {
190190
}
191191
defer r.Close()
192192

193+
for _, f := range r.File {
194+
// ZIP paths use forward slashes on every platform. Reject backslashes
195+
// rather than letting Windows reinterpret a filename from a Unix
196+
// repository as a directory hierarchy.
197+
if strings.ContainsRune(f.Name, '\\') {
198+
return fmt.Errorf("%q: illegal file path", f.Name)
199+
}
200+
}
201+
193202
outputBase := filepath.Clean(dest) + string(os.PathSeparator)
194203

195204
for _, f := range r.File {

internal/batches/workspace/bind_workspace_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,54 @@ func TestDockerBindWorkspaceCreator_Create(t *testing.T) {
143143
})
144144
}
145145

146+
func TestUnzipRejectsUnsafeArchivePaths(t *testing.T) {
147+
tests := []string{
148+
`.git\config`,
149+
`hooks\pre-commit`,
150+
}
151+
152+
for _, name := range tests {
153+
t.Run(name, func(t *testing.T) {
154+
archivePath := zipUpFiles(t, t.TempDir(), map[string]string{name: "malicious"})
155+
dest := t.TempDir()
156+
157+
if err := unzip(context.Background(), archivePath, dest); err == nil {
158+
t.Fatal("expected unsafe archive path to be rejected")
159+
}
160+
161+
entries, err := os.ReadDir(dest)
162+
if err != nil {
163+
t.Fatal(err)
164+
}
165+
if len(entries) != 0 {
166+
t.Fatalf("archive was partially extracted: %v", entries)
167+
}
168+
})
169+
}
170+
}
171+
172+
func TestUnzipAllowsSafeControlPaths(t *testing.T) {
173+
files := map[string]string{
174+
".git_config": "config",
175+
"hooks_pre-commit": "hook",
176+
}
177+
archivePath := zipUpFiles(t, t.TempDir(), files)
178+
dest := t.TempDir()
179+
180+
if err := unzip(context.Background(), archivePath, dest); err != nil {
181+
t.Fatal(err)
182+
}
183+
for name, want := range files {
184+
have, err := os.ReadFile(filepath.Join(dest, name))
185+
if err != nil {
186+
t.Fatal(err)
187+
}
188+
if string(have) != want {
189+
t.Errorf("%s: got %q, want %q", name, have, want)
190+
}
191+
}
192+
}
193+
146194
func TestDockerBindWorkspace_ApplyDiff(t *testing.T) {
147195
// Create a zip file for all the other tests to use.
148196
fakeFilesTmpDir := t.TempDir()

internal/batches/workspace/git.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import (
99
)
1010

1111
func runGitCmd(ctx context.Context, dir string, args ...string) ([]byte, error) {
12+
// Repository contents are untrusted. Keep hooks disabled even if a command
13+
// encounters an attacker-controlled local Git configuration.
14+
args = append([]string{"-c", "core.hooksPath=/dev/null"}, args...)
1215
cmd := exec.CommandContext(ctx, "git", args...)
1316
cmd.Env = []string{
1417
// Don't use the system wide git config.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package workspace
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestRunGitCmdDisablesHooks(t *testing.T) {
11+
dir := t.TempDir()
12+
ctx := context.Background()
13+
14+
if _, err := runGitCmd(ctx, dir, "init", "--quiet"); err != nil {
15+
t.Fatal(err)
16+
}
17+
config, err := os.OpenFile(filepath.Join(dir, ".git", "config"), os.O_APPEND|os.O_WRONLY, 0)
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
if _, err := config.WriteString("[core]\n\thooksPath = hooks\n"); err != nil {
22+
t.Fatal(err)
23+
}
24+
if err := config.Close(); err != nil {
25+
t.Fatal(err)
26+
}
27+
if err := os.Mkdir(filepath.Join(dir, "hooks"), 0755); err != nil {
28+
t.Fatal(err)
29+
}
30+
if err := os.WriteFile(filepath.Join(dir, "hooks", "pre-commit"), []byte("#!/bin/sh\necho hook-ran > hook-ran\n"), 0755); err != nil {
31+
t.Fatal(err)
32+
}
33+
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("test\n"), 0644); err != nil {
34+
t.Fatal(err)
35+
}
36+
37+
if _, err := runGitCmd(ctx, dir, "add", "README.md"); err != nil {
38+
t.Fatal(err)
39+
}
40+
if _, err := runGitCmd(ctx, dir, "commit", "--quiet", "-m", "test"); err != nil {
41+
t.Fatal(err)
42+
}
43+
if _, err := os.Stat(filepath.Join(dir, "hook-ran")); !os.IsNotExist(err) {
44+
t.Fatalf("pre-commit hook ran: %v", err)
45+
}
46+
}

0 commit comments

Comments
 (0)