Skip to content

Commit 94a4067

Browse files
committed
feat: block file transfers
1 parent a51076a commit 94a4067

File tree

4 files changed

+100
-1
lines changed

4 files changed

+100
-1
lines changed

agent/agent.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ type Options struct {
9191
ModifiedProcesses chan []*agentproc.Process
9292
// ProcessManagementTick is used for testing process priority management.
9393
ProcessManagementTick <-chan time.Time
94+
BlockFileTransfer bool
9495
}
9596

9697
type Client interface {
@@ -184,6 +185,7 @@ func New(options Options) Agent {
184185
modifiedProcs: options.ModifiedProcesses,
185186
processManagementTick: options.ProcessManagementTick,
186187
logSender: agentsdk.NewLogSender(options.Logger),
188+
blockFileTransfer: options.BlockFileTransfer,
187189

188190
prometheusRegistry: prometheusRegistry,
189191
metrics: newAgentMetrics(prometheusRegistry),
@@ -239,6 +241,7 @@ type agent struct {
239241
sessionToken atomic.Pointer[string]
240242
sshServer *agentssh.Server
241243
sshMaxTimeout time.Duration
244+
blockFileTransfer bool
242245

243246
lifecycleUpdate chan struct{}
244247
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
@@ -277,6 +280,7 @@ func (a *agent) init() {
277280
AnnouncementBanners: func() *[]codersdk.BannerConfig { return a.announcementBanners.Load() },
278281
UpdateEnv: a.updateCommandEnv,
279282
WorkingDirectory: func() string { return a.manifest.Load().Directory },
283+
BlockFileTransfer: a.blockFileTransfer,
280284
})
281285
if err != nil {
282286
panic(err)

agent/agent_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,51 @@ func TestAgent_SCP(t *testing.T) {
970970
require.NoError(t, err)
971971
}
972972

973+
func TestAgent_FileTransferBlocked(t *testing.T) {
974+
t.Parallel()
975+
976+
content := "hello world"
977+
978+
t.Run("SCP", func(t *testing.T) {
979+
t.Parallel()
980+
981+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
982+
defer cancel()
983+
984+
//nolint:dogsled
985+
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
986+
o.BlockFileTransfer = true
987+
})
988+
sshClient, err := conn.SSHClient(ctx)
989+
require.NoError(t, err)
990+
defer sshClient.Close()
991+
scpClient, err := scp.NewClientBySSH(sshClient)
992+
require.NoError(t, err)
993+
defer scpClient.Close()
994+
tempFile := filepath.Join(t.TempDir(), "scp")
995+
err = scpClient.CopyFile(context.Background(), strings.NewReader(content), tempFile, "0755")
996+
require.Error(t, err)
997+
require.Contains(t, err.Error(), agentssh.BlockedFileTransferErrorMessage)
998+
})
999+
1000+
t.Run("SFTP", func(t *testing.T) {
1001+
t.Parallel()
1002+
1003+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1004+
defer cancel()
1005+
1006+
//nolint:dogsled
1007+
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
1008+
o.BlockFileTransfer = true
1009+
})
1010+
sshClient, err := conn.SSHClient(ctx)
1011+
require.NoError(t, err)
1012+
defer sshClient.Close()
1013+
_, err = sftp.NewClient(sshClient)
1014+
require.NoError(t, err)
1015+
})
1016+
}
1017+
9731018
func TestAgent_EnvironmentVariables(t *testing.T) {
9741019
t.Parallel()
9751020
key := "EXAMPLE"

agent/agentssh/agentssh.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ const (
5252
// MagicProcessCmdlineJetBrains is a string in a process's command line that
5353
// uniquely identifies it as JetBrains software.
5454
MagicProcessCmdlineJetBrains = "idea.vendor.name=JetBrains"
55+
56+
// BlockedFileTransferErrorCode indicates that SSH server restricted the raw command from performing
57+
// the file transfer.
58+
BlockedFileTransferErrorCode = 2
59+
BlockedFileTransferErrorMessage = "File transfer has been disabled."
5560
)
5661

5762
// Config sets configuration parameters for the agent SSH server.
@@ -74,6 +79,8 @@ type Config struct {
7479
// X11SocketDir is the directory where X11 sockets are created. Default is
7580
// /tmp/.X11-unix.
7681
X11SocketDir string
82+
// BlockFileTransfer restricts use of file transfer applications.
83+
BlockFileTransfer bool
7784
}
7885

7986
type Server struct {
@@ -272,6 +279,14 @@ func (s *Server) sessionHandler(session ssh.Session) {
272279
extraEnv = append(extraEnv, fmt.Sprintf("DISPLAY=:%d.0", x11.ScreenNumber))
273280
}
274281

282+
if s.fileTransferBlocked(session) {
283+
// Response format: <status_code><message body>\n
284+
errorMessage := fmt.Sprintf("\x02%s\n", BlockedFileTransferErrorMessage)
285+
_, _ = session.Write([]byte(errorMessage))
286+
_ = session.Exit(BlockedFileTransferErrorCode)
287+
return
288+
}
289+
275290
switch ss := session.Subsystem(); ss {
276291
case "":
277292
case "sftp":
@@ -322,6 +337,32 @@ func (s *Server) sessionHandler(session ssh.Session) {
322337
_ = session.Exit(0)
323338
}
324339

340+
func (s *Server) fileTransferBlocked(session ssh.Session) bool {
341+
if !s.config.BlockFileTransfer {
342+
return false // file transfers are permitted
343+
}
344+
// File transfers are restricted.
345+
346+
if session.Subsystem() == "sftp" {
347+
return true // sftp mode is forbidden
348+
}
349+
350+
cmd := session.Command()
351+
if len(cmd) == 0 {
352+
return false // no command?
353+
}
354+
355+
c := cmd[0]
356+
c = filepath.Base(c) // in case the binary is absolute path, /usr/sbin/scp
357+
358+
switch c {
359+
case "nc", "rsync", "scp", "sftp":
360+
return true // forbidden command
361+
default:
362+
return false
363+
}
364+
}
365+
325366
func (s *Server) sessionStart(logger slog.Logger, session ssh.Session, extraEnv []string) (retErr error) {
326367
ctx := session.Context()
327368
env := append(session.Environ(), extraEnv...)

cli/agent.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
4848
slogHumanPath string
4949
slogJSONPath string
5050
slogStackdriverPath string
51+
blockFileTransfer bool
5152
)
5253
cmd := &serpent.Command{
5354
Use: "agent",
@@ -314,6 +315,8 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
314315
// Intentionally set this to nil. It's mainly used
315316
// for testing.
316317
ModifiedProcesses: nil,
318+
319+
BlockFileTransfer: blockFileTransfer,
317320
})
318321

319322
promHandler := agent.PrometheusMetricsHandler(prometheusRegistry, logger)
@@ -412,11 +415,17 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
412415
{
413416
Name: "Stackdriver Log Location",
414417
Description: "Output Stackdriver compatible logs to a given file.",
415-
Flag: "log-stackdriver",
416418
Env: "CODER_AGENT_LOGGING_STACKDRIVER",
417419
Default: "",
418420
Value: serpent.StringOf(&slogStackdriverPath),
419421
},
422+
{
423+
Flag: "block-file-transfer",
424+
Default: "false",
425+
Env: "CODER_BLOCK_FILE_TRANSFER",
426+
Description: "Block file transfer using known applications: nc, rsync, scp, sftp",
427+
Value: serpent.BoolOf(&blockFileTransfer),
428+
},
420429
}
421430

422431
return cmd

0 commit comments

Comments
 (0)