Skip to content

Commit

Permalink
Use SafeJoin in the uploadHandler (#821)
Browse files Browse the repository at this point in the history
  • Loading branch information
alessio-perugini authored Sep 18, 2023
1 parent 716e7aa commit e740ad9
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 6 deletions.
8 changes: 6 additions & 2 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ func uploadHandler(c *gin.Context) {
}

for _, extraFile := range data.ExtraFiles {
path := filepath.Join(tmpdir, extraFile.Filename)
path, err := utilities.SafeJoin(tmpdir, extraFile.Filename)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
filePaths = append(filePaths, path)
log.Printf("Saving %s on %s", extraFile.Filename, path)

Expand All @@ -150,7 +154,7 @@ func uploadHandler(c *gin.Context) {
return
}

err := os.WriteFile(path, extraFile.Hex, 0644)
err = os.WriteFile(path, extraFile.Hex, 0644)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
Expand Down
37 changes: 37 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/arduino/arduino-create-agent/config"
"github.com/arduino/arduino-create-agent/gen/tools"
"github.com/arduino/arduino-create-agent/upload"
v2 "github.com/arduino/arduino-create-agent/v2"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
Expand All @@ -48,6 +49,42 @@ func TestValidSignatureKey(t *testing.T) {
require.NotNil(t, key)
}

func TestUploadHandlerAgainstEvilFileNames(t *testing.T) {
r := gin.New()
r.POST("/", uploadHandler)
ts := httptest.NewServer(r)

uploadEvilFileName := Upload{
Port: "/dev/ttyACM0",
Board: "arduino:avr:uno",
Extra: upload.Extra{Network: true},
Hex: []byte("test"),
Filename: "../evil.txt",
ExtraFiles: []additionalFile{{Hex: []byte("test"), Filename: "../evil.txt"}},
}
uploadEvilExtraFile := Upload{
Port: "/dev/ttyACM0",
Board: "arduino:avr:uno",
Extra: upload.Extra{Network: true},
Hex: []byte("test"),
Filename: "file.txt",
ExtraFiles: []additionalFile{{Hex: []byte("test"), Filename: "../evil.txt"}},
}

for _, request := range []Upload{uploadEvilFileName, uploadEvilExtraFile} {
payload, err := json.Marshal(request)
require.NoError(t, err)

resp, err := http.Post(ts.URL, "encoding/json", bytes.NewBuffer(payload))
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)

body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(body), "unsafe path join")
}
}

func TestInstallToolDifferentContentType(t *testing.T) {
r := gin.New()
goa := v2.Server(config.GetDataDir().String())
Expand Down
27 changes: 24 additions & 3 deletions utilities/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
)

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

func saveFileonTempDir(tmpDir, filename string, data io.Reader) (string, error) {
path, err := SafeJoin(tmpDir, filename)
if err != nil {
return "", err
}
// Determine filename
filename, err = filepath.Abs(tmpdir + "/" + filename)
filename, err = filepath.Abs(path)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -141,3 +149,16 @@ func Unzip(zippath string, destination string) (err error) {
}
return
}

// SafeJoin performs a filepath.Join of 'parent' and 'subdir' but returns an error
// if the resulting path points outside of 'parent'.
func SafeJoin(parent, subdir string) (string, error) {
res := filepath.Join(parent, subdir)
if !strings.HasSuffix(parent, string(os.PathSeparator)) {
parent += string(os.PathSeparator)
}
if !strings.HasPrefix(res, parent) {
return res, fmt.Errorf("unsafe path join: '%s' with '%s'", parent, subdir)
}
return res, nil
}
48 changes: 48 additions & 0 deletions utilities/utilities_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package utilities

import (
"bytes"
"fmt"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/require"
)

func TestSaveFileonTemp(t *testing.T) {
filename := "file"
tmpDir := t.TempDir()

path, err := saveFileonTempDir(tmpDir, filename, bytes.NewBufferString("TEST"))
require.NoError(t, err)
require.Equal(t, filepath.Join(tmpDir, filename), path)
}

func TestSaveFileonTempDirWithEvilName(t *testing.T) {
evilFileNames := []string{
"/",
"..",
"../",
"../evil.txt",
"../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
"some/path/../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
"/../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
"/some/path/../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
}
if runtime.GOOS == "windows" {
evilFileNames = []string{
"..\\",
"..\\evil.txt",
"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
"some\\path\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
"\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
"\\some\\path\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
}
}
for _, evilFileName := range evilFileNames {
_, err := saveFileonTempDir(t.TempDir(), evilFileName, bytes.NewBufferString("TEST"))
require.Error(t, err, fmt.Sprintf("with filename: '%s'", evilFileName))
require.ErrorContains(t, err, "unsafe path join")
}
}
7 changes: 6 additions & 1 deletion v2/pkgs/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"strings"

"github.com/arduino/arduino-create-agent/gen/tools"
"github.com/arduino/arduino-create-agent/utilities"
"github.com/codeclysm/extract/v3"
)

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

err := os.RemoveAll(filepath.Join(c.Folder, path))
err = os.RemoveAll(pathToRemove)
if err != nil {
return nil, err
}
Expand Down
28 changes: 28 additions & 0 deletions v2/pkgs/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/arduino/arduino-create-agent/gen/indexes"
"github.com/arduino/arduino-create-agent/gen/tools"
"github.com/arduino/arduino-create-agent/v2/pkgs"
"github.com/stretchr/testify/require"
)

// TestTools performs a series of operations about tools, ensuring it behaves as expected.
Expand Down Expand Up @@ -150,6 +151,33 @@ func TestTools(t *testing.T) {
if len(installed) != 1 {
t.Fatalf("expected %d == %d (%s)", len(installed), 1, "len(installed)")
}

t.Run("payload containing evil names", func(t *testing.T) {
evilFileNames := []string{
"/",
"..",
"../",
"../evil.txt",
"../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
"some/path/../../../../../../../../../../../../../../../../../../../../tmp/evil.txt",
}
if runtime.GOOS == "windows" {
evilFileNames = []string{
"..\\",
"..\\evil.txt",
"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
"some\\path\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\tmp\\evil.txt",
}
}
for _, evilFileName := range evilFileNames {
// Here we could inject malicious name also in the Packager and Version field.
// Since the path is made by joining all of these 3 fields, we're using only the Name,
// as it won't change the result and let us keep the test small and readable.
_, err := service.Remove(ctx, &tools.ToolPayload{Name: evilFileName})
require.Error(t, err, evilFileName)
require.ErrorContains(t, err, "unsafe path join")
}
})
}

func strpoint(s string) *string {
Expand Down

0 comments on commit e740ad9

Please sign in to comment.