Skip to content

Commit 702bdea

Browse files
committed
Merge branch 'use-safe-join' into test-rc
2 parents 2ad821a + e523731 commit 702bdea

File tree

6 files changed

+148
-6
lines changed

6 files changed

+148
-6
lines changed

conn.go

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

135135
for _, extraFile := range data.ExtraFiles {
136-
path := filepath.Join(tmpdir, extraFile.Filename)
136+
path, err := utilities.SafeJoin(tmpdir, extraFile.Filename)
137+
if err != nil {
138+
c.String(http.StatusBadRequest, err.Error())
139+
return
140+
}
137141
filePaths = append(filePaths, path)
138142
log.Printf("Saving %s on %s", extraFile.Filename, path)
139143

@@ -143,7 +147,7 @@ func uploadHandler(c *gin.Context) {
143147
return
144148
}
145149

146-
err := os.WriteFile(path, extraFile.Hex, 0644)
150+
err = os.WriteFile(path, extraFile.Hex, 0644)
147151
if err != nil {
148152
c.String(http.StatusBadRequest, err.Error())
149153
return

main_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929

3030
"github.com/arduino/arduino-create-agent/config"
3131
"github.com/arduino/arduino-create-agent/gen/tools"
32+
"github.com/arduino/arduino-create-agent/upload"
3233
v2 "github.com/arduino/arduino-create-agent/v2"
3334
"github.com/gin-gonic/gin"
3435
"github.com/stretchr/testify/require"
@@ -114,3 +115,38 @@ func TestInstallToolV2(t *testing.T) {
114115
})
115116
}
116117
}
118+
func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
119+
r := gin.New()
120+
r.POST("/", uploadHandler)
121+
ts := httptest.NewServer(r)
122+
123+
uploadEvilFileName := Upload{
124+
Port: "/dev/ttyACM0",
125+
Board: "arduino:avr:uno",
126+
Extra: upload.Extra{Network: true},
127+
Hex: []byte("test"),
128+
Filename: "../evil.txt",
129+
ExtraFiles: []additionalFile{{Hex: []byte("test"), Filename: "../evil.txt"}},
130+
}
131+
uploadEvilExtraFile := Upload{
132+
Port: "/dev/ttyACM0",
133+
Board: "arduino:avr:uno",
134+
Extra: upload.Extra{Network: true},
135+
Hex: []byte("test"),
136+
Filename: "file.txt",
137+
ExtraFiles: []additionalFile{{Hex: []byte("test"), Filename: "../evil.txt"}},
138+
}
139+
140+
for _, request := range []Upload{uploadEvilFileName, uploadEvilExtraFile} {
141+
payload, err := json.Marshal(request)
142+
require.NoError(t, err)
143+
144+
resp, err := http.Post(ts.URL, "encoding/json", bytes.NewBuffer(payload))
145+
require.NoError(t, err)
146+
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
147+
148+
body, err := io.ReadAll(resp.Body)
149+
require.NoError(t, err)
150+
require.Contains(t, string(body), "unsafe path join")
151+
}
152+
}

utilities/utilities.go

+25-3
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ import (
2525
"encoding/hex"
2626
"encoding/pem"
2727
"errors"
28+
"fmt"
2829
"io"
2930
"os"
3031
"os/exec"
3132
"path"
3233
"path/filepath"
3334

35+
"strings"
36+
3437
"github.com/arduino/arduino-create-agent/globals"
3538
)
3639

@@ -40,15 +43,21 @@ import (
4043
// Returns an error if the filename doesn't form a valid path.
4144
//
4245
// Note that path could be defined and still there could be an error.
43-
func SaveFileonTempDir(filename string, data io.Reader) (path string, err error) {
44-
// Create Temp Directory
46+
func SaveFileonTempDir(filename string, data io.Reader) (string, error) {
4547
tmpdir, err := os.MkdirTemp("", "arduino-create-agent")
4648
if err != nil {
4749
return "", errors.New("Could not create temp directory to store downloaded file. Do you have permissions?")
4850
}
51+
return saveFileonTempDir(tmpdir, filename, data)
52+
}
4953

54+
func saveFileonTempDir(tmpDir, filename string, data io.Reader) (string, error) {
55+
path, err := SafeJoin(tmpDir, filename)
56+
if err != nil {
57+
return "", err
58+
}
5059
// Determine filename
51-
filename, err = filepath.Abs(tmpdir + "/" + filename)
60+
filename, err = filepath.Abs(path)
5261
if err != nil {
5362
return "", err
5463
}
@@ -168,3 +177,16 @@ func VerifyInput(input string, signature string) error {
168177
d := h.Sum(nil)
169178
return rsa.VerifyPKCS1v15(rsaKey, crypto.SHA256, d, sign)
170179
}
180+
181+
// SafeJoin performs a filepath.Join of 'parent' and 'subdir' but returns an error
182+
// if the resulting path points outside of 'parent'.
183+
func SafeJoin(parent, subdir string) (string, error) {
184+
res := filepath.Join(parent, subdir)
185+
if !strings.HasSuffix(parent, string(os.PathSeparator)) {
186+
parent += string(os.PathSeparator)
187+
}
188+
if !strings.HasPrefix(res, parent) {
189+
return res, fmt.Errorf("unsafe path join: '%s' with '%s'", parent, subdir)
190+
}
191+
return res, nil
192+
}

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+
}

v2/pkgs/tools.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,12 @@ func (c *Tools) install(ctx context.Context, path, url, checksum string) (*tools
223223
// Remove deletes the tool folder from Tools Folder
224224
func (c *Tools) Remove(ctx context.Context, payload *tools.ToolPayload) (*tools.Operation, error) {
225225
path := filepath.Join(payload.Packager, payload.Name, payload.Version)
226+
pathToRemove, err := utilities.SafeJoin(c.Folder, path)
227+
if err != nil {
228+
return nil, err
229+
}
226230

227-
err := os.RemoveAll(filepath.Join(c.Folder, path))
231+
err = os.RemoveAll(pathToRemove)
228232
if err != nil {
229233
return nil, err
230234
}

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)