@@ -61,6 +61,17 @@ export class Daffodil {
6161 ignoreFile = ".scpignore" ,
6262 verbose = false ,
6363 } ) {
64+ // Validate required parameters
65+ if ( ! remoteUser || typeof remoteUser !== "string" ) {
66+ throw new Error ( "remoteUser is required and must be a string" ) ;
67+ }
68+ if ( ! remoteHost || typeof remoteHost !== "string" ) {
69+ throw new Error ( "remoteHost is required and must be a string" ) ;
70+ }
71+ if ( port && ( typeof port !== "number" || port < 1 || port > 65535 ) ) {
72+ throw new Error ( "port must be a number between 1 and 65535" ) ;
73+ }
74+
6475 this . remoteUser = remoteUser ;
6576 this . remoteHost = remoteHost ;
6677 this . remotePath = remotePath ;
@@ -88,6 +99,17 @@ export class Daffodil {
8899 return new Date ( ) . toISOString ( ) ;
89100 }
90101
102+ /**
103+ * Escape shell arguments to prevent command injection
104+ * @param {string } arg - Argument to escape
105+ * @returns {string } Escaped argument
106+ * @private
107+ */
108+ escapeShellArg ( arg ) {
109+ // Replace single quotes with '\'' and wrap in single quotes
110+ return `'${ String ( arg ) . replace ( / ' / g, "'\\''" ) } '` ;
111+ }
112+
91113 /**
92114 * Log message with timestamp if verbose is enabled
93115 * @param {string } message - Message to log
@@ -238,7 +260,7 @@ export class Daffodil {
238260 this . logTimeConsumption ( "SSH Connection" , startTime ) ;
239261
240262 const ensurePath = path . posix . resolve ( this . remotePath ) ;
241- await this . ssh . execCommand ( `mkdir -p ${ ensurePath } ` ) ;
263+ await this . ssh . execCommand ( `mkdir -p ${ this . escapeShellArg ( ensurePath ) } ` ) ;
242264 this . log ( `Verified or created remote path: ${ ensurePath } ` , "blue" ) ;
243265 return ; // ✅ Success, skip remaining
244266 } catch ( err ) {
@@ -269,7 +291,10 @@ export class Daffodil {
269291 ) ;
270292 }
271293
272- process . exit ( 1 ) ;
294+ // Throw error instead of process.exit for library code
295+ throw new Error (
296+ "SSH connection failed: No valid SSH keys worked. Please ensure your SSH keys are properly configured."
297+ ) ;
273298 }
274299
275300 loadIgnoreList ( ) {
@@ -338,7 +363,7 @@ export class Daffodil {
338363 const startTime = Date . now ( ) ;
339364 const fullPath = path . posix . join ( this . remotePath , dirName ) ;
340365 this . log ( `Creating remote directory: ${ fullPath } ` , "blue" ) ;
341- await this . sshCommand ( `mkdir -p ${ fullPath } ` ) ;
366+ await this . sshCommand ( `mkdir -p ${ this . escapeShellArg ( fullPath ) } ` ) ;
342367 this . logTimeConsumption ( `Create directory: ${ fullPath } ` , startTime ) ;
343368 }
344369
@@ -542,7 +567,7 @@ export class Daffodil {
542567 if ( this . verbose ) {
543568 this . log ( "Removing old archive from remote if exists" , "blue" ) ;
544569 }
545- await this . ssh . execCommand ( `rm -f " ${ remoteArchivePath } " || true` ) ;
570+ await this . ssh . execCommand ( `rm -f ${ this . escapeShellArg ( remoteArchivePath ) } || true` ) ;
546571
547572 // Transfer the archive file
548573 await this . ssh . putFile ( archivePath , remoteArchivePath ) ;
@@ -562,11 +587,11 @@ export class Daffodil {
562587 }
563588
564589 // Ensure destination directory exists
565- await this . ssh . execCommand ( `mkdir -p " ${ destinationPath } " || true` ) ;
590+ await this . ssh . execCommand ( `mkdir -p ${ this . escapeShellArg ( destinationPath ) } || true` ) ;
566591
567592 // Extract archive (overwrite existing files)
568593 const extractResult = await this . ssh . execCommand (
569- `cd " ${ destinationPath } " && tar -xzf " ${ remoteArchivePath } " && rm -f " ${ remoteArchivePath } " `
594+ `cd ${ this . escapeShellArg ( destinationPath ) } && tar -xzf ${ this . escapeShellArg ( remoteArchivePath ) } && rm -f ${ this . escapeShellArg ( remoteArchivePath ) } `
570595 ) ;
571596
572597 if ( extractResult . code !== 0 ) {
@@ -608,7 +633,7 @@ export class Daffodil {
608633 if ( this . verbose ) {
609634 this . log ( "Cleaning up remote archive after error" , "yellow" ) ;
610635 }
611- await this . ssh . execCommand ( `rm -f " ${ remoteArchivePath } " || true` ) ;
636+ await this . ssh . execCommand ( `rm -f ${ this . escapeShellArg ( remoteArchivePath ) } || true` ) ;
612637 } catch ( cleanupErr ) {
613638 // Ignore cleanup errors
614639 if ( this . verbose ) {
0 commit comments