ssh2shell 2.0.11 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ssh2shell.js +1094 -1024
- package/package.json +1 -1
- package/src/ssh2shell.coffee +126 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh2shell",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"author": "cmp-202",
|
|
5
5
|
"description": "A wrapper class for ssh2 to run multiple sequential commands in an SSH shell session, handle command responses, and tunnel through to other hosts using nested host objects",
|
|
6
6
|
"main": "./lib/ssh2shell.js",
|
package/src/ssh2shell.coffee
CHANGED
|
@@ -314,36 +314,93 @@ class SSH2Shell extends Stream
|
|
|
314
314
|
|
|
315
315
|
_sshConnect: =>
|
|
316
316
|
|
|
317
|
-
#
|
|
317
|
+
# Add ssh commandline options from host.server.ssh
|
|
318
318
|
sshFlags = "-x"
|
|
319
319
|
sshOptions = ""
|
|
320
|
+
|
|
321
|
+
# 1. Helper function to ensure individual values are safely wrapped
|
|
322
|
+
# to prevent spaces or characters from escaping parameter flags
|
|
323
|
+
shellQuote = (value) -> "'" + String(value).replace(/'/g, "'\\''") + "'"
|
|
324
|
+
|
|
325
|
+
# 2. Strong control whitelist pattern ensuring basic parameters
|
|
326
|
+
# don't contain command separators or layout manipulation tokens
|
|
327
|
+
shellControlChars = /[;&|`$\n\r]/
|
|
328
|
+
|
|
320
329
|
if @sshObj.server.ssh
|
|
330
|
+
# Safely handle protocol configurations
|
|
321
331
|
sshFlags += @sshObj.server.ssh.forceProtocolVersion if @sshObj.server.ssh.forceProtocolVersion
|
|
322
332
|
sshFlags += @sshObj.server.ssh.forceAddressType if @sshObj.server.ssh.forceAddressType
|
|
323
333
|
sshFlags += "T" if @sshObj.server.ssh.disablePseudoTTY
|
|
324
334
|
sshFlags += "t" if @sshObj.server.ssh.forcePseudoTTY
|
|
325
335
|
sshFlags += "v" if @sshObj.server.ssh.verbose
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
|
|
337
|
+
# 3. Intercept and safely escape single parameter properties
|
|
338
|
+
if @sshObj.server.ssh.cipherSpec
|
|
339
|
+
if shellControlChars.test(@sshObj.server.ssh.cipherSpec)
|
|
340
|
+
@.emit 'error', "Malicious characters detected in SSH cipherSpec.", "Security Alert", true
|
|
341
|
+
return
|
|
342
|
+
sshOptions += " -c " + shellQuote(@sshObj.server.ssh.cipherSpec)
|
|
343
|
+
|
|
344
|
+
if @sshObj.server.ssh.escape
|
|
345
|
+
if shellControlChars.test(@sshObj.server.ssh.escape)
|
|
346
|
+
@.emit 'error', "Malicious characters detected in SSH escape sequence.", "Security Alert", true
|
|
347
|
+
return
|
|
348
|
+
sshOptions += " -e " + shellQuote(@sshObj.server.ssh.escape)
|
|
349
|
+
|
|
350
|
+
if @sshObj.server.ssh.logFile
|
|
351
|
+
if shellControlChars.test(@sshObj.server.ssh.logFile)
|
|
352
|
+
@.emit 'error', "Malicious characters detected in SSH logFile path.", "Security Alert", true
|
|
353
|
+
return
|
|
354
|
+
sshOptions += " -E " + shellQuote(@sshObj.server.ssh.logFile)
|
|
355
|
+
|
|
356
|
+
if @sshObj.server.ssh.configFile
|
|
357
|
+
if shellControlChars.test(@sshObj.server.ssh.configFile)
|
|
358
|
+
@.emit 'error', "Malicious characters detected in SSH configFile path.", "Security Alert", true
|
|
359
|
+
return
|
|
360
|
+
sshOptions += " -F " + shellQuote(@sshObj.server.ssh.configFile)
|
|
361
|
+
|
|
362
|
+
if @sshObj.server.ssh.identityFile
|
|
363
|
+
if shellControlChars.test(@sshObj.server.ssh.identityFile)
|
|
364
|
+
@.emit 'error', "Malicious characters detected in SSH identityFile path.", "Security Alert", true
|
|
365
|
+
return
|
|
366
|
+
sshOptions += " -i " + shellQuote(@sshObj.server.ssh.identityFile)
|
|
367
|
+
|
|
368
|
+
if @sshObj.server.ssh.loginName
|
|
369
|
+
if shellControlChars.test(@sshObj.server.ssh.loginName)
|
|
370
|
+
@.emit 'error', "Malicious characters detected in SSH loginName.", "Security Alert", true
|
|
371
|
+
return
|
|
372
|
+
sshOptions += " -l " + shellQuote(@sshObj.server.ssh.loginName)
|
|
373
|
+
|
|
374
|
+
if @sshObj.server.ssh.macSpec
|
|
375
|
+
if shellControlChars.test(@sshObj.server.ssh.macSpec)
|
|
376
|
+
@.emit 'error', "Malicious characters detected in SSH macSpec.", "Security Alert", true
|
|
377
|
+
return
|
|
378
|
+
sshOptions += " -m " + shellQuote(@sshObj.server.ssh.macSpec)
|
|
379
|
+
|
|
380
|
+
# Validate custom option blocks safely
|
|
335
381
|
for option, value of (@sshObj.server.ssh.options or {})
|
|
336
382
|
if shellControlChars.test(option) or shellControlChars.test(value)
|
|
337
|
-
@.emit 'error',"Malicious characters detected in SSH options.","Security Alert",true
|
|
383
|
+
@.emit 'error', "Malicious characters detected in SSH options.", "Security Alert", true
|
|
338
384
|
return
|
|
339
385
|
safeOption = String(option).replace(/[^a-zA-Z0-9]/g, "")
|
|
340
386
|
safeValue = shellQuote(value)
|
|
341
387
|
sshOptions += " -o #{safeOption}=#{safeValue}"
|
|
388
|
+
|
|
389
|
+
# Wrap target dynamic properties to ensure security boundaries hold up
|
|
342
390
|
sshOptions += ' -o "StrictHostKeyChecking=no"'
|
|
343
|
-
|
|
391
|
+
|
|
392
|
+
# Sanitize user context fields going straight into the string command
|
|
393
|
+
safePort = String(@sshObj.server.port or 22).replace(/[^0-9]/g, "")
|
|
394
|
+
sshOptions += " -p #{safePort}"
|
|
395
|
+
|
|
396
|
+
safeUser = shellQuote(@sshObj.server.userName)
|
|
397
|
+
safeHost = shellQuote(@sshObj.server.host)
|
|
398
|
+
|
|
344
399
|
@sshObj.sshAuth = false
|
|
345
400
|
@sshObj.sshToNextHost = true
|
|
346
|
-
|
|
401
|
+
|
|
402
|
+
# 4. Perfectly combined secure connection command string
|
|
403
|
+
@command = "ssh #{sshFlags} #{sshOptions} #{safeUser}@#{safeHost}"
|
|
347
404
|
@.emit 'msg', "#{@sshObj.server.host}: SSH command: connect" if @sshObj.debug
|
|
348
405
|
@_runCommand(@command)
|
|
349
406
|
|
|
@@ -475,7 +532,7 @@ class SSH2Shell extends Stream
|
|
|
475
532
|
@.emit 'msg', "#{@sshObj.server.host}: Load Defaults" if @sshObj.debug
|
|
476
533
|
@command = ""
|
|
477
534
|
@_buffer = ""
|
|
478
|
-
@sshObj.sshToNextHost
|
|
535
|
+
@sshObj.sshToNextHost = false
|
|
479
536
|
@sshObj.connectedMessage = "Connected" unless @sshObj.connectedMessage
|
|
480
537
|
@sshObj.readyMessage = "Ready" unless @sshObj.readyMessage
|
|
481
538
|
@sshObj.closedMessage = "Closed" unless @sshObj.closedMessage
|
|
@@ -490,11 +547,11 @@ class SSH2Shell extends Stream
|
|
|
490
547
|
@sshObj.passwordPrompt = ":" unless @sshObj.passwordPrompt
|
|
491
548
|
@sshObj.passphrasePrompt = ":" unless @sshObj.passphrasePrompt
|
|
492
549
|
@sshObj.passPromptText = "Password" unless @sshObj.passPromptText
|
|
493
|
-
@sshObj.enter =
|
|
494
|
-
@sshObj.
|
|
550
|
+
@sshObj.enter = if /^[\r\n]+$/.test(@sshObj.enter or "") then @sshObj.enter else "\n"
|
|
551
|
+
@sshObj.textColorFilter = "(\[{1}[0-9;]+m{1})" unless @sshObj.textColorFilter
|
|
495
552
|
@sshObj.disableColorFilter = false unless @sshObj.disableColorFilter is true
|
|
553
|
+
@sshObj.asciiFilter = "[^\r\n\x20-\x7e]" unless @sshObj.asciiFilter
|
|
496
554
|
@sshObj.disableASCIIFilter = false unless @sshObj.disableASCIIFilter is true
|
|
497
|
-
@sshObj.textColorFilter = "(\[{1}[0-9;]+m{1})" unless @sshObj.textColorFilter
|
|
498
555
|
@sshObj.exitCommands = [] unless @sshObj.exitCommands
|
|
499
556
|
@sshObj.pwSent = false
|
|
500
557
|
@sshObj.sshAuth = false
|
|
@@ -505,12 +562,56 @@ class SSH2Shell extends Stream
|
|
|
505
562
|
@sshObj.pty = true unless @sshObj.pty
|
|
506
563
|
@idleTime = @sshObj.idleTimeOut ? 5000
|
|
507
564
|
@dataIdleTime = @sshObj.dataIdleTime ? 500
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
565
|
+
|
|
566
|
+
escapeRegex = (str) -> String(str).replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
|
|
567
|
+
|
|
568
|
+
# 1. Compile ASCII Filter with Safe Fallback
|
|
569
|
+
unless @asciiFilter
|
|
570
|
+
if @sshObj.asciiFilter is "[^\r\n\x20-\x7e]"
|
|
571
|
+
@asciiFilter = /[^\t\r\n\x20-\x7e]/g
|
|
572
|
+
else
|
|
573
|
+
# Only allow basic structural character classes/ranges; reject dangerous loops (+, *, ?, nested groups)
|
|
574
|
+
safeAsciiPattern = /^\[\^?[a-zA-Z0-9\s\\x00-\\x7f\-\[\]]+\]$/
|
|
575
|
+
|
|
576
|
+
if safeAsciiPattern.test(@sshObj.asciiFilter)
|
|
577
|
+
@asciiFilter = new RegExp(@sshObj.asciiFilter, "g")
|
|
578
|
+
else
|
|
579
|
+
# Graceful Fallback: Warn the user via your debug pipeline and run the safe internal fallback
|
|
580
|
+
if @sshObj.debug
|
|
581
|
+
@emit 'msg', "#{@sshObj.server.host}: AsciiFilter: incorrect regular expression reverting to default: " + @sshObj.asciiFilter
|
|
582
|
+
|
|
583
|
+
# Force set the safe pre-compiled literal so CodeQL passes statically
|
|
584
|
+
@asciiFilter = /[^\t\r\n\x20-\x7e]/g
|
|
585
|
+
|
|
586
|
+
# 2. Compile Text Color Filter with Safe Fallback
|
|
587
|
+
unless @textColorFilter
|
|
588
|
+
if @sshObj.textColorFilter is "(\[{1}[0-9;]+m{1})"
|
|
589
|
+
@textColorFilter = /\x1b\[[0-9;]*m/g
|
|
590
|
+
else
|
|
591
|
+
# Match standard ANSI styles safely, rejecting any ReDoS structures
|
|
592
|
+
safeColorPattern = /^(\\x1b|\\033)?\\?\[[0-9;]*\*?[a-zA-Z]$/
|
|
593
|
+
|
|
594
|
+
if safeColorPattern.test(@sshObj.textColorFilter)
|
|
595
|
+
@textColorFilter = new RegExp(@sshObj.textColorFilter, "g")
|
|
596
|
+
else
|
|
597
|
+
if @sshObj.debug
|
|
598
|
+
@emit 'msg', "#{@sshObj.server.host}: TextColorFilter: incorrect regular expression reverting to default: " + @sshObj.textColorFilter
|
|
599
|
+
@textColorFilter = /\x1b\[[0-9;]*m/g
|
|
600
|
+
|
|
601
|
+
# 3. Prompts: Fully sanitized at runtime via escapeRegex (never need fallbacks because they are cast as pure literals)
|
|
602
|
+
unless @passwordPrompt
|
|
603
|
+
cleanText = escapeRegex(@sshObj.passPromptText or "Password")
|
|
604
|
+
cleanPrompt = escapeRegex(@sshObj.passwordPrompt or ":")
|
|
605
|
+
@passwordPrompt = new RegExp("#{cleanText}[^\\n]*#{cleanPrompt}\\s?$", "i")
|
|
606
|
+
|
|
607
|
+
unless @passphrasePrompt
|
|
608
|
+
cleanText = escapeRegex(@sshObj.passPromptText or "Password")
|
|
609
|
+
cleanPass = escapeRegex(@sshObj.passphrasePrompt or ":")
|
|
610
|
+
@passphrasePrompt = new RegExp("#{cleanText}[^\\n]*#{cleanPass}\\s?$", "i")
|
|
611
|
+
|
|
612
|
+
unless @standardPrompt
|
|
613
|
+
cleanStandard = escapeRegex(@sshObj.standardPrompt or ">$%#")
|
|
614
|
+
@standardPrompt = new RegExp("[#{cleanStandard}]\\s?$")
|
|
514
615
|
|
|
515
616
|
@sshObj.onCommandProcessing = @sshObj.onCommandProcessing ? ( command, response, sshObj, stream ) =>
|
|
516
617
|
|
|
@@ -523,7 +624,8 @@ class SSH2Shell extends Stream
|
|
|
523
624
|
@.emit 'msg', "#{@sshObj.server.host}: Timeout command: #{command} response: #{response}" if @sshObj.verbose
|
|
524
625
|
|
|
525
626
|
@.emit "error", "#{@sshObj.server.host}: Command timed out after #{@.idleTime/1000} seconds", "Timeout", true, (err, type)=>
|
|
526
|
-
|
|
627
|
+
@sshObj.sessionText += @_buffer
|
|
628
|
+
@_buffer = ""
|
|
527
629
|
|
|
528
630
|
@.on "keyboard-interactive", @sshObj.onKeyboardInteractive if typeof @sshObj.onKeyboardInteractive == 'function'
|
|
529
631
|
@.on "error", @sshObj.onError if typeof @sshObj.onError == 'function'
|