@@ -145,26 +145,39 @@ func (a *App) SSH(ctx context.Context, sshArgs []string, opts sshOptions) (err e
145145type cpOptions struct {
146146 sshOptions
147147 recursive bool // -r
148+ expand bool // -e
148149}
149150
150151func 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 : `
157158The 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-
163160As with the UNIX cp command, the first argument specifies the source and the last
164161specifies the destination; additional sources may be specified after the first,
165162if the destination is a directory.
166163
167164The -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