X Tutup
Skip to content

Commit fc1de3a

Browse files
authored
Merge pull request cli#4564 from cli/e-flag
gh cs cp: disable remote shell expansion unless -e flag
2 parents b96ccb4 + f683d6c commit fc1de3a

File tree

2 files changed

+35
-8
lines changed

2 files changed

+35
-8
lines changed

internal/codespaces/ssh.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ func Shell(ctx context.Context, log logger, sshArgs []string, port int, destinat
2727

2828
// Copy runs an scp command over the specified port. The arguments may
2929
// include flags and non-flags, optionally separated by "--".
30-
// Remote files are indicated by a "remote:" prefix, and are resolved
31-
// relative to the remote user's home directory.
30+
//
31+
// Remote files indicated by a "remote:" prefix are resolved relative
32+
// to the remote user's home directory, and are subject to shell expansion
33+
// on the remote host; see https://lwn.net/Articles/835962/.
3234
func Copy(ctx context.Context, scpArgs []string, port int, destination string) error {
3335
// Beware: invalid syntax causes scp to exit 1 with
3436
// no error message, so don't let that happen.

pkg/cmd/codespace/ssh.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,26 +145,39 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
145145
type cpOptions struct {
146146
sshOptions
147147
recursive bool // -r
148+
expand bool // -e
148149
}
149150

150151
func newCpCmd(app *App) *cobra.Command {
151152
var opts cpOptions
152153

153154
cpCmd := &cobra.Command{
154-
Use: "cp [-r] srcs... dest",
155+
Use: "cp [-e] [-r] srcs... dest",
155156
Short: "Copy files between local and remote file systems",
156157
Long: `
157158
The cp command copies files between the local and remote file systems.
158159
159-
A 'remote:' prefix on any file name argument indicates that it refers to
160-
the file system of the remote (Codespace) machine. It is resolved relative
161-
to the home directory of the remote user.
162-
163160
As with the UNIX cp command, the first argument specifies the source and the last
164161
specifies the destination; additional sources may be specified after the first,
165162
if the destination is a directory.
166163
167164
The -r (recursive) flag is required if any source is a directory.
165+
166+
A 'remote:' prefix on any file name argument indicates that it refers to
167+
the file system of the remote (Codespace) machine. It is resolved relative
168+
to the home directory of the remote user.
169+
170+
By default, remote file names are interpreted literally. With the -e flag,
171+
each such argument is treated in the manner of scp, as a Bash expression to
172+
be evaluated on the remote machine, subject to expansion of tildes, braces,
173+
globs, environment variables, and backticks, as in these examples:
174+
175+
$ gh codespace cp -e README.md 'remote:/workspace/$RepositoryName/'
176+
$ gh codespace cp -e 'remote:~/*.go' ./gofiles/
177+
$ gh codespace cp -e 'remote:/workspace/myproj/go.{mod,sum}' ./gofiles/
178+
179+
For security, do not use the -e flag with arguments provided by untrusted
180+
users; see https://lwn.net/Articles/835962/ for discussion.
168181
`,
169182
RunE: func(cmd *cobra.Command, args []string) error {
170183
return app.Copy(cmd.Context(), args, opts)
@@ -173,6 +186,7 @@ The -r (recursive) flag is required if any source is a directory.
173186

174187
// We don't expose all sshOptions.
175188
cpCmd.Flags().BoolVarP(&opts.recursive, "recursive", "r", false, "Recursively copy directories")
189+
cpCmd.Flags().BoolVarP(&opts.expand, "expand", "e", false, "Expand remote file names on remote shell")
176190
cpCmd.Flags().StringVarP(&opts.codespace, "codespace", "c", "", "Name of the codespace")
177191
return cpCmd
178192
}
@@ -188,7 +202,18 @@ func (a *App) Copy(ctx context.Context, args []string, opts cpOptions) (err erro
188202
}
189203
opts.scpArgs = append(opts.scpArgs, "--")
190204
for _, arg := range args {
191-
if !filepath.IsAbs(arg) && !strings.HasPrefix(arg, "remote:") {
205+
if rest := strings.TrimPrefix(arg, "remote:"); rest != arg {
206+
// scp treats each filename argument as a shell expression,
207+
// subjecting it to expansion of environment variables, braces,
208+
// tilde, backticks, globs and so on. Because these present a
209+
// security risk (see https://lwn.net/Articles/835962/), we
210+
// disable them by shell-escaping the argument unless the user
211+
// provided the -e flag.
212+
if !opts.expand {
213+
arg = `remote:'` + strings.Replace(rest, `'`, `'\''`, -1) + `'`
214+
}
215+
216+
} else if !filepath.IsAbs(arg) {
192217
// scp treats a colon in the first path segment as a host identifier.
193218
// Escape it by prepending "./".
194219
// TODO(adonovan): test on Windows, including with a c:\\foo path.

0 commit comments

Comments
 (0)
X Tutup