Skip to content

Commit bca91d7

Browse files
committed
fix: prevent directory traversal attack when writing request files
1 parent 02473a2 commit bca91d7

5 files changed

Lines changed: 176 additions & 3 deletions

File tree

internal/engine/docker.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"io"
99
"os"
1010
"os/exec"
11-
"path/filepath"
1211
"strconv"
1312
"strings"
1413
"time"
@@ -54,7 +53,10 @@ func (e *Docker) Exec(req Request) Execution {
5453
if e.cmd.Entry != "" {
5554
// write request files to the temp directory
5655
err = e.writeFiles(dir, req.Files)
57-
if err != nil {
56+
var argErr ArgumentError
57+
if errors.As(err, &argErr) {
58+
return Fail(req.ID, err)
59+
} else if err != nil {
5860
err = NewExecutionError("write files to temp dir", err)
5961
return Fail(req.ID, err)
6062
}
@@ -171,7 +173,12 @@ func (e *Docker) writeFiles(dir string, files Files) error {
171173
if name == "" {
172174
name = e.cmd.Entry
173175
}
174-
path := filepath.Join(dir, name)
176+
var path string
177+
path, err = fileio.JoinDir(dir, name)
178+
if err != nil {
179+
err = NewArgumentError(fmt.Sprintf("files[%s]", name), err)
180+
return false
181+
}
175182
err = fileio.WriteFile(path, content, 0444)
176183
return err == nil
177184
})

internal/engine/docker_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package engine
22

33
import (
4+
"fmt"
45
"strings"
56
"testing"
67

@@ -231,6 +232,29 @@ func TestDockerRun(t *testing.T) {
231232
t.Errorf("Stderr: unexpected value: %s", out.Stderr)
232233
}
233234
})
235+
236+
t.Run("directory traversal attack", func(t *testing.T) {
237+
mem.Clear()
238+
const fileName = "../../opt/codapi/codapi"
239+
engine := NewDocker(dockerCfg, "python", "run")
240+
req := Request{
241+
ID: "http_42",
242+
Sandbox: "python",
243+
Command: "run",
244+
Files: map[string]string{
245+
"": "print('hello world')",
246+
fileName: "hehe",
247+
},
248+
}
249+
out := engine.Exec(req)
250+
if out.OK {
251+
t.Error("OK: expected false")
252+
}
253+
want := fmt.Sprintf("files[%s]: invalid name", fileName)
254+
if out.Stderr != want {
255+
t.Errorf("Stderr: unexpected value: %s", out.Stderr)
256+
}
257+
})
234258
}
235259

236260
func TestDockerExec(t *testing.T) {

internal/engine/engine.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,25 @@ func (err ExecutionError) Unwrap() error {
6262
return err.inner
6363
}
6464

65+
// An ArgumentError is returned if code execution failed
66+
// due to the invalid value of the request agrument.
67+
type ArgumentError struct {
68+
name string
69+
reason error
70+
}
71+
72+
func NewArgumentError(name string, reason error) ArgumentError {
73+
return ArgumentError{name: name, reason: reason}
74+
}
75+
76+
func (err ArgumentError) Error() string {
77+
return err.name + ": " + err.reason.Error()
78+
}
79+
80+
func (err ArgumentError) Unwrap() error {
81+
return err.reason
82+
}
83+
6584
// Files are a collection of files to be executed by the engine.
6685
type Files map[string]string
6786

internal/fileio/fileio.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,35 @@ func WriteFile(path, content string, perm fs.FileMode) (err error) {
7979
return os.WriteFile(path, data, perm)
8080
}
8181

82+
// JoinDir joins a directory path with a relative file path,
83+
// making sure that the resulting path is still inside the directory.
84+
// Returns an error otherwise.
85+
func JoinDir(dir string, name string) (string, error) {
86+
if dir == "" {
87+
return "", errors.New("invalid dir")
88+
}
89+
90+
cleanName := filepath.Clean(name)
91+
if cleanName == "" {
92+
return "", errors.New("invalid name")
93+
}
94+
if cleanName == "." || cleanName == "/" || filepath.IsAbs(cleanName) {
95+
return "", errors.New("invalid name")
96+
}
97+
98+
path := filepath.Join(dir, cleanName)
99+
100+
dirPrefix := filepath.Clean(dir)
101+
if dirPrefix != "/" {
102+
dirPrefix += string(os.PathSeparator)
103+
}
104+
if !strings.HasPrefix(path, dirPrefix) {
105+
return "", errors.New("invalid name")
106+
}
107+
108+
return path, nil
109+
}
110+
82111
// MkdirTemp creates a new temporary directory with given permissions
83112
// and returns the pathname of the new directory.
84113
func MkdirTemp(perm fs.FileMode) (string, error) {

internal/fileio/fileio_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,100 @@ func TestWriteFile(t *testing.T) {
160160
})
161161
}
162162

163+
func TestJoinDir(t *testing.T) {
164+
tests := []struct {
165+
name string
166+
dir string
167+
filename string
168+
want string
169+
wantErr bool
170+
}{
171+
{
172+
name: "regular join",
173+
dir: "/home/user",
174+
filename: "docs/report.txt",
175+
want: "/home/user/docs/report.txt",
176+
wantErr: false,
177+
},
178+
{
179+
name: "join with dot",
180+
dir: "/home/user",
181+
filename: ".",
182+
want: "",
183+
wantErr: true,
184+
},
185+
{
186+
name: "join with absolute path",
187+
dir: "/home/user",
188+
filename: "/etc/passwd",
189+
want: "",
190+
wantErr: true,
191+
},
192+
{
193+
name: "join with parent directory",
194+
dir: "/home/user",
195+
filename: "../user2/docs/report.txt",
196+
want: "",
197+
wantErr: true,
198+
},
199+
{
200+
name: "empty directory",
201+
dir: "",
202+
filename: "report.txt",
203+
want: "",
204+
wantErr: true,
205+
},
206+
{
207+
name: "empty filename",
208+
dir: "/home/user",
209+
filename: "",
210+
want: "",
211+
wantErr: true,
212+
},
213+
{
214+
name: "directory with trailing slash",
215+
dir: "/home/user/",
216+
filename: "docs/report.txt",
217+
want: "/home/user/docs/report.txt",
218+
wantErr: false,
219+
},
220+
{
221+
name: "filename with leading slash",
222+
dir: "/home/user",
223+
filename: "/docs/report.txt",
224+
want: "",
225+
wantErr: true,
226+
},
227+
{
228+
name: "root directory",
229+
dir: "/",
230+
filename: "report.txt",
231+
want: "/report.txt",
232+
wantErr: false,
233+
},
234+
{
235+
name: "dot dot slash filename",
236+
dir: "/home/user",
237+
filename: "..",
238+
want: "",
239+
wantErr: true,
240+
},
241+
}
242+
243+
for _, tt := range tests {
244+
t.Run(tt.name, func(t *testing.T) {
245+
got, err := JoinDir(tt.dir, tt.filename)
246+
if (err != nil) != tt.wantErr {
247+
t.Errorf("JoinDir() error = %v, wantErr %v", err, tt.wantErr)
248+
return
249+
}
250+
if got != tt.want {
251+
t.Errorf("JoinDir() = %v, want %v", got, tt.want)
252+
}
253+
})
254+
}
255+
}
256+
163257
func TestMkdirTemp(t *testing.T) {
164258
t.Run("default permissions", func(t *testing.T) {
165259
const perm = 0755

0 commit comments

Comments
 (0)