Skip to content

Use SafeJoin in the uploadHandler #821

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Comment on lines +153 to +164
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could be a good candidate for go-paths-helper

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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