Skip to content

Commit e740ad9

Browse files
Use SafeJoin in the uploadHandler (#821)
1 parent 716e7aa commit e740ad9

File tree

6 files changed

+149
-6
lines changed

6 files changed

+149
-6
lines changed

Diff for: conn.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,11 @@ func uploadHandler(c *gin.Context) {
140140
}
141141

142142
for _, extraFile := range data.ExtraFiles {
143-
path := filepath.Join(tmpdir, extraFile.Filename)
143+
path, err := utilities.SafeJoin(tmpdir, extraFile.Filename)
144+
if err != nil {
145+
c.String(http.StatusBadRequest, err.Error())
146+
return
147+
}
144148
filePaths = append(filePaths, path)
145149
log.Printf("Saving %s on %s", extraFile.Filename, path)
146150

@@ -150,7 +154,7 @@ func uploadHandler(c *gin.Context) {
150154
return
151155
}
152156

153-
err := os.WriteFile(path, extraFile.Hex, 0644)
157+
err = os.WriteFile(path, extraFile.Hex, 0644)
154158
if err != nil {
155159
c.String(http.StatusBadRequest, err.Error())
156160
return

Diff for: main_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
"github.com/arduino/arduino-create-agent/config"
3030
"github.com/arduino/arduino-create-agent/gen/tools"
31+
"github.com/arduino/arduino-create-agent/upload"
3132
v2 "github.com/arduino/arduino-create-agent/v2"
3233
"github.com/gin-gonic/gin"
3334
"github.com/stretchr/testify/require"
@@ -48,6 +49,42 @@ func TestValidSignatureKey(t *testing.T) {
4849
require.NotNil(t, key)
4950
}
5051

52+
func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
53+
r := gin.New()
54+
r.POST("/", uploadHandler)
55+
ts := httptest.NewServer(r)
56+
57+
uploadEvilFileName := Upload{
58+
Port: "/dev/ttyACM0",
59+
Board: "arduino:avr:uno",
60+
Extra: upload.Extra{Network: true},
61+
Hex: []byte("test"),
62+
Filename: "../evil.txt",
63+
ExtraFiles: []additionalFile{{Hex: []byte("test"), Filename: "../evil.txt"}},
64+
}
65+
uploadEvilExtraFile := Upload{
66+
Port: "/dev/ttyACM0",
67+
Board: "arduino:avr:uno",
68+
Extra: upload.Extra{Network: true},
69+
Hex: []byte("test"),
70+
Filename: "file.txt",
71+
ExtraFiles: []additionalFile{{Hex: []byte("test"), Filename: "../evil.txt"}},
72+
}
73+
74+
for _, request := range []Upload{uploadEvilFileName, uploadEvilExtraFile} {
75+
payload, err := json.Marshal(request)
76+
require.NoError(t, err)
77+
78+
resp, err := http.Post(ts.URL, "encoding/json", bytes.NewBuffer(payload))
79+
require.NoError(t, err)
80+
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
81+
82+
body, err := io.ReadAll(resp.Body)
83+
require.NoError(t, err)
84+
require.Contains(t, string(body), "unsafe path join")
85+
}
86+
}
87+
5188
func TestInstallToolDifferentContentType(t *testing.T) {
5289
r := gin.New()
5390
goa := v2.Server(config.GetDataDir().String())

Diff for: utilities/utilities.go

+24-3
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ import (
1919
"archive/zip"
2020
"bytes"
2121
"errors"
22+
"fmt"
2223
"io"
2324
"os"
2425
"os/exec"
2526
"path"
2627
"path/filepath"
28+
"strings"
2729
)
2830

2931
// SaveFileonTempDir creates a temp directory and saves the file data as the
@@ -32,15 +34,21 @@ import (
3234
// Returns an error if the filename doesn't form a valid path.
3335
//
3436
// Note that path could be defined and still there could be an error.
35-
func SaveFileonTempDir(filename string, data io.Reader) (path string, err error) {
36-
// Create Temp Directory
37+
func SaveFileonTempDir(filename string, data io.Reader) (string, error) {
3738
tmpdir, err := os.MkdirTemp("", "arduino-create-agent")
3839
if err != nil {
3940
return "", errors.New("Could not create temp directory to store downloaded file. Do you have permissions?")
4041
}
42+
return saveFileonTempDir(tmpdir, filename, data)
43+
}
4144

45+
func saveFileonTempDir(tmpDir, filename string, data io.Reader) (string, error) {
46+
path, err := SafeJoin(tmpDir, filename)
47+
if err != nil {
48+
return "", err
49+
}
4250
// Determine filename
43-
filename, err = filepath.Abs(tmpdir + "/" + filename)
51+
filename, err = filepath.Abs(path)
4452
if err != nil {
4553
return "", err
4654
}
@@ -141,3 +149,16 @@ func Unzip(zippath string, destination string) (err error) {
141149
}
142150
return
143151
}
152+
153+
// SafeJoin performs a filepath.Join of 'parent' and 'subdir' but returns an error
154+
// if the resulting path points outside of 'parent'.
155+
func SafeJoin(parent, subdir string) (string, error) {
156+
res := filepath.Join(parent, subdir)
157+
if !strings.HasSuffix(parent, string(os.PathSeparator)) {
158+
parent += string(os.PathSeparator)
159+
}
160+
if !strings.HasPrefix(res, parent) {
161+
return res, fmt.Errorf("unsafe path join: '%s' with '%s'", parent, subdir)
162+
}
163+
return res, nil
164+
}

Diff for: utilities/utilities_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package utilities
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"path/filepath"
7+
"runtime"
8+
"testing"
9+
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestSaveFileonTemp(t *testing.T) {
14+
filename := "file"
15+
tmpDir := t.TempDir()
16+
17+
path, err := saveFileonTempDir(tmpDir, filename, bytes.NewBufferString("TEST"))
18+
require.NoError(t, err)
19+
require.Equal(t, filepath.Join(tmpDir, filename), path)
20+
}
21+
22+
func TestSaveFileonTempDirWithEvilName(t *testing.T) {
23+
evilFileNames := []string{
24+
"/",
25+
"..",
26+
"../",
27+
"../evil.txt",
28+
"../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
29+
"some/path/../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
30+
"/../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
31+
"/some/path/../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
32+
}
33+
if runtime.GOOS == "windows" {
34+
evilFileNames = []string{
35+
"..\\",
36+
"..\\evil.txt",
37+
"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
38+
"some\\path\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
39+
"\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
40+
"\\some\\path\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
41+
}
42+
}
43+
for _, evilFileName := range evilFileNames {
44+
_, err := saveFileonTempDir(t.TempDir(), evilFileName, bytes.NewBufferString("TEST"))
45+
require.Error(t, err, fmt.Sprintf("with filename: '%s'", evilFileName))
46+
require.ErrorContains(t, err, "unsafe path join")
47+
}
48+
}

Diff for: v2/pkgs/tools.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"strings"
3232

3333
"github.com/arduino/arduino-create-agent/gen/tools"
34+
"github.com/arduino/arduino-create-agent/utilities"
3435
"github.com/codeclysm/extract/v3"
3536
)
3637

@@ -216,8 +217,12 @@ func (c *Tools) install(ctx context.Context, path, url, checksum string) (*tools
216217
// Remove deletes the tool folder from Tools Folder
217218
func (c *Tools) Remove(ctx context.Context, payload *tools.ToolPayload) (*tools.Operation, error) {
218219
path := filepath.Join(payload.Packager, payload.Name, payload.Version)
220+
pathToRemove, err := utilities.SafeJoin(c.Folder, path)
221+
if err != nil {
222+
return nil, err
223+
}
219224

220-
err := os.RemoveAll(filepath.Join(c.Folder, path))
225+
err = os.RemoveAll(pathToRemove)
221226
if err != nil {
222227
return nil, err
223228
}

Diff for: v2/pkgs/tools_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/arduino/arduino-create-agent/gen/indexes"
2828
"github.com/arduino/arduino-create-agent/gen/tools"
2929
"github.com/arduino/arduino-create-agent/v2/pkgs"
30+
"github.com/stretchr/testify/require"
3031
)
3132

3233
// TestTools performs a series of operations about tools, ensuring it behaves as expected.
@@ -150,6 +151,33 @@ func TestTools(t *testing.T) {
150151
if len(installed) != 1 {
151152
t.Fatalf("expected %d == %d (%s)", len(installed), 1, "len(installed)")
152153
}
154+
155+
t.Run("payload containing evil names", func(t *testing.T) {
156+
evilFileNames := []string{
157+
"/",
158+
"..",
159+
"../",
160+
"../evil.txt",
161+
"../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
162+
"some/path/../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
163+
}
164+
if runtime.GOOS == "windows" {
165+
evilFileNames = []string{
166+
"..\\",
167+
"..\\evil.txt",
168+
"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
169+
"some\\path\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
170+
}
171+
}
172+
for _, evilFileName := range evilFileNames {
173+
// Here we could inject malicious name also in the Packager and Version field.
174+
// Since the path is made by joining all of these 3 fields, we're using only the Name,
175+
// as it won't change the result and let us keep the test small and readable.
176+
_, err := service.Remove(ctx, &tools.ToolPayload{Name: evilFileName})
177+
require.Error(t, err, evilFileName)
178+
require.ErrorContains(t, err, "unsafe path join")
179+
}
180+
})
153181
}
154182

155183
func strpoint(s string) *string {

0 commit comments

Comments
 (0)