diff --git a/changelog/unreleased/issue-3932 b/changelog/unreleased/issue-3932 new file mode 100644 index 000000000..6802a78ba --- /dev/null +++ b/changelog/unreleased/issue-3932 @@ -0,0 +1,15 @@ +Enhancement: Improve handling of ErrDot errors in rclone and sftp backends + +Since Go 1.19, restic can no longer implicitly run relative executables which +are found in the current directory (e.g. `rclone` if it's found in `.`). This +is a security feature of Go to prevent against running unintended and possibly +harmful executables. + +The error message for this was just "cannot run executable found relative to +current directory". This has now been improved to yield a more specific error +message, informing the user how to explicitly allow running the executable +using the `-o rclone.program` and `-o sftp.command` extended options with `./`. + +https://github.com/restic/restic/issues/3932 +https://pkg.go.dev/os/exec#hdr-Executables_in_the_current_directory +https://go.dev/blog/path-security diff --git a/doc/030_preparing_a_new_repo.rst b/doc/030_preparing_a_new_repo.rst index 9e92e98da..ff9e7c8ff 100644 --- a/doc/030_preparing_a_new_repo.rst +++ b/doc/030_preparing_a_new_repo.rst @@ -633,6 +633,13 @@ initiate a new repository in the path ``bar`` in the remote ``foo``: Restic takes care of starting and stopping rclone. +.. note:: If you get an error message saying "cannot implicitly run relative + executable rclone found in current directory", this means that an + rclone executable was found in the current directory. For security + reasons restic will not run this implicitly, instead you have to + use the ``-o rclone.program=./rclone`` extended option to override + this security check and explicitly tell restic to use the executable. + As a more concrete example, suppose you have configured a remote named ``b2prod`` for Backblaze B2 with rclone, with a bucket called ``yggdrasil``. You can then use rclone to list files in the bucket like this: diff --git a/internal/backend/errdot_119.go b/internal/backend/errdot_119.go new file mode 100644 index 000000000..3676a099d --- /dev/null +++ b/internal/backend/errdot_119.go @@ -0,0 +1,20 @@ +//go:build go1.19 +// +build go1.19 + +// This file provides a function to check whether an error from cmd.Start() is +// exec.ErrDot which was introduced in Go 1.19. +// This function is needed so that we can perform this check only for Go 1.19 and +// up, whereas for older versions we use a dummy/stub in the file errdot_old.go. +// Once the minimum Go version restic supports is 1.19, remove this file and +// replace any calls to it with the corresponding code as per below. + +package backend + +import ( + "errors" + "os/exec" +) + +func IsErrDot(err error) bool { + return errors.Is(err, exec.ErrDot) +} diff --git a/internal/backend/errdot_old.go b/internal/backend/errdot_old.go new file mode 100644 index 000000000..92a58ad25 --- /dev/null +++ b/internal/backend/errdot_old.go @@ -0,0 +1,13 @@ +//go:build !go1.19 +// +build !go1.19 + +// This file provides a stub for IsErrDot() for Go versions below 1.19. +// See the corresponding file errdot_119.go for more information. +// Once the minimum Go version restic supports is 1.19, remove this file +// and perform the actions listed in errdot_119.go. + +package backend + +func IsErrDot(err error) bool { + return false +} diff --git a/internal/backend/rclone/backend.go b/internal/backend/rclone/backend.go index cccc52384..df1d13d02 100644 --- a/internal/backend/rclone/backend.go +++ b/internal/backend/rclone/backend.go @@ -85,6 +85,9 @@ func run(command string, args ...string) (*StdioConn, *sync.WaitGroup, func() er err = errW } if err != nil { + if backend.IsErrDot(err) { + return nil, nil, nil, errors.Errorf("cannot implicitly run relative executable %v found in current directory, use -o rclone.program=./ to override", cmd.Path) + } return nil, nil, nil, err } diff --git a/internal/backend/sftp/sftp.go b/internal/backend/sftp/sftp.go index a1b8f5cdf..f6e82a402 100644 --- a/internal/backend/sftp/sftp.go +++ b/internal/backend/sftp/sftp.go @@ -80,7 +80,10 @@ func startClient(cfg Config) (*SFTP, error) { bg, err := backend.StartForeground(cmd) if err != nil { - return nil, errors.Wrap(err, "cmd.Start") + if backend.IsErrDot(err) { + return nil, errors.Errorf("cannot implicitly run relative executable %v found in current directory, use -o sftp.command=./ to override", cmd.Path) + } + return nil, err } // wait in a different goroutine