@@ -10,6 +10,7 @@ import (
1010 "net"
1111 "net/netip"
1212 "os"
13+ "path/filepath"
1314 "reflect"
1415 "runtime"
1516 "runtime/pprof"
@@ -188,6 +189,12 @@ func configSSH(l *logrus.Logger, ssh *sshd.SSHServer, c *config.C) (func(), erro
188189}
189190
190191func attachCommands (l * logrus.Logger , c * config.C , ssh * sshd.SSHServer , f * Interface ) {
192+ // sandboxDir defaults to a dir in temp. The intention is that end user will
193+ // create this dir as needed. Overriding this config value to "" allows
194+ // writing to anywhere in the system.
195+ defaultDir := filepath .Join (os .TempDir (), "nebula-debug" )
196+ sandboxDir := c .GetString ("sshd.sandbox_dir" , defaultDir )
197+
191198 ssh .RegisterCommand (& sshd.Command {
192199 Name : "list-hostmap" ,
193200 ShortDescription : "List all known previously connected hosts" ,
@@ -246,7 +253,9 @@ func attachCommands(l *logrus.Logger, c *config.C, ssh *sshd.SSHServer, f *Inter
246253 ssh .RegisterCommand (& sshd.Command {
247254 Name : "start-cpu-profile" ,
248255 ShortDescription : "Starts a cpu profile and write output to the provided file, ex: `cpu-profile.pb.gz`" ,
249- Callback : sshStartCpuProfile ,
256+ Callback : func (fs any , a []string , w sshd.StringWriter ) error {
257+ return sshStartCpuProfile (sandboxDir , fs , a , w )
258+ },
250259 })
251260
252261 ssh .RegisterCommand (& sshd.Command {
@@ -261,7 +270,9 @@ func attachCommands(l *logrus.Logger, c *config.C, ssh *sshd.SSHServer, f *Inter
261270 ssh .RegisterCommand (& sshd.Command {
262271 Name : "save-heap-profile" ,
263272 ShortDescription : "Saves a heap profile to the provided path, ex: `heap-profile.pb.gz`" ,
264- Callback : sshGetHeapProfile ,
273+ Callback : func (fs any , a []string , w sshd.StringWriter ) error {
274+ return sshGetHeapProfile (sandboxDir , fs , a , w )
275+ },
265276 })
266277
267278 ssh .RegisterCommand (& sshd.Command {
@@ -273,7 +284,9 @@ func attachCommands(l *logrus.Logger, c *config.C, ssh *sshd.SSHServer, f *Inter
273284 ssh .RegisterCommand (& sshd.Command {
274285 Name : "save-mutex-profile" ,
275286 ShortDescription : "Saves a mutex profile to the provided path, ex: `mutex-profile.pb.gz`" ,
276- Callback : sshGetMutexProfile ,
287+ Callback : func (fs any , a []string , w sshd.StringWriter ) error {
288+ return sshGetMutexProfile (sandboxDir , fs , a , w )
289+ },
277290 })
278291
279292 ssh .RegisterCommand (& sshd.Command {
@@ -506,13 +519,43 @@ func sshListLighthouseMap(lightHouse *LightHouse, a any, w sshd.StringWriter) er
506519 return nil
507520}
508521
509- func sshStartCpuProfile (fs any , a []string , w sshd.StringWriter ) error {
522+ // sshSanitizeFilePath validates that the given file path is within the sandbox directory.
523+ // If sandboxDir is empty, the path is returned as-is for backwards compatibility.
524+ func sshSanitizeFilePath (sandboxDir , filePath string ) (string , error ) {
525+ if sandboxDir == "" {
526+ return filePath , nil
527+ }
528+
529+ // Clean and resolve the path relative to the sandbox directory
530+ if ! filepath .IsAbs (filePath ) {
531+ filePath = filepath .Join (sandboxDir , filePath )
532+ }
533+ cleaned := filepath .Clean (filePath )
534+
535+ // Ensure the resolved path is within the sandbox directory
536+ cleanedSandbox := filepath .Clean (sandboxDir )
537+ if cleaned == cleanedSandbox {
538+ return "" , fmt .Errorf ("path %q resolves to the sandbox directory itself %q" , filePath , sandboxDir )
539+ }
540+ if ! strings .HasPrefix (cleaned , cleanedSandbox + string (filepath .Separator )) {
541+ return "" , fmt .Errorf ("path %q is outside the sandbox directory %q" , filePath , sandboxDir )
542+ }
543+
544+ return cleaned , nil
545+ }
546+
547+ func sshStartCpuProfile (sandboxDir string , fs any , a []string , w sshd.StringWriter ) error {
510548 if len (a ) == 0 {
511549 err := w .WriteLine ("No path to write profile provided" )
512550 return err
513551 }
514552
515- file , err := os .Create (a [0 ])
553+ filePath , err := sshSanitizeFilePath (sandboxDir , a [0 ])
554+ if err != nil {
555+ return w .WriteLine (err .Error ())
556+ }
557+
558+ file , err := os .Create (filePath )
516559 if err != nil {
517560 err = w .WriteLine (fmt .Sprintf ("Unable to create profile file: %s" , err ))
518561 return err
@@ -676,12 +719,17 @@ func sshChangeRemote(ifce *Interface, fs any, a []string, w sshd.StringWriter) e
676719 return w .WriteLine ("Changed" )
677720}
678721
679- func sshGetHeapProfile (fs any , a []string , w sshd.StringWriter ) error {
722+ func sshGetHeapProfile (sandboxDir string , fs any , a []string , w sshd.StringWriter ) error {
680723 if len (a ) == 0 {
681724 return w .WriteLine ("No path to write profile provided" )
682725 }
683726
684- file , err := os .Create (a [0 ])
727+ filePath , err := sshSanitizeFilePath (sandboxDir , a [0 ])
728+ if err != nil {
729+ return w .WriteLine (err .Error ())
730+ }
731+
732+ file , err := os .Create (filePath )
685733 if err != nil {
686734 err = w .WriteLine (fmt .Sprintf ("Unable to create profile file: %s" , err ))
687735 return err
@@ -712,12 +760,17 @@ func sshMutexProfileFraction(fs any, a []string, w sshd.StringWriter) error {
712760 return w .WriteLine (fmt .Sprintf ("New value: %d. Old value: %d" , newRate , oldRate ))
713761}
714762
715- func sshGetMutexProfile (fs any , a []string , w sshd.StringWriter ) error {
763+ func sshGetMutexProfile (sandboxDir string , fs any , a []string , w sshd.StringWriter ) error {
716764 if len (a ) == 0 {
717765 return w .WriteLine ("No path to write profile provided" )
718766 }
719767
720- file , err := os .Create (a [0 ])
768+ filePath , err := sshSanitizeFilePath (sandboxDir , a [0 ])
769+ if err != nil {
770+ return w .WriteLine (err .Error ())
771+ }
772+
773+ file , err := os .Create (filePath )
721774 if err != nil {
722775 return w .WriteLine (fmt .Sprintf ("Unable to create profile file: %s" , err ))
723776 }
0 commit comments