From a96cf69f663120780281a54aa0f762e534d58283 Mon Sep 17 00:00:00 2001
From: Marek Kalnik <marekk@bam.tech>
Date: Wed, 26 Jun 2024 15:29:52 +0200
Subject: [PATCH] Fix command injection and remove useless util.format

Util.format is not recommended and sometimes unnecessary here.
String literals should be enough
---
 lib/simctl-extensions.js | 12 ++++++++---
 simctl.js                | 45 ++++++++++++++++++++--------------------
 2 files changed, 32 insertions(+), 25 deletions(-)

diff --git a/lib/simctl-extensions.js b/lib/simctl-extensions.js
index 8f0ff70..99ff4a8 100644
--- a/lib/simctl-extensions.js
+++ b/lib/simctl-extensions.js
@@ -28,8 +28,14 @@ const fs = require('fs')
 const util = require('util')
 const Tail = require('tail').Tail
 
+const escapeShellArg = function (arg) {
+  return typeof arg === 'string' ? arg.replace(/(["`])/g, '\\$1') : arg;
+}
+
 const extensions = {
   start: function (deviceid) {
+    deviceid = escapeShellArg(deviceid)
+
     let isAtLeastXcode9 = false
 
     let command = 'xcodebuild -version'
@@ -46,7 +52,7 @@ const extensions = {
 
     if (isAtLeastXcode9) {
       // Xcode 9 or greater
-      command = util.format('xcrun simctl list -j')
+      command = `xcrun simctl list -j`
       let res = shell.exec(command, { silent: true })
       if (res.code !== 0) {
         console.error('Could not get device list.')
@@ -62,7 +68,7 @@ const extensions = {
         console.error('Simulator already running.')
         return
       }
-      command = util.format('xcrun simctl boot "%s"', deviceid)
+      command = `xcrun simctl boot "${deviceid}"`
       res = shell.exec(command, { silent: true })
 
       if (res.code !== 0) {
@@ -74,7 +80,7 @@ const extensions = {
       return shell.exec(command, { silent: true })
     } else {
       // Xcode 8 or older
-      command = util.format('xcrun instruments -w "%s"', deviceid)
+      command = `xcrun instruments -w "${deviceid}"`
       return shell.exec(command, { silent: true })
     }
   },
diff --git a/simctl.js b/simctl.js
index c5df486..52da619 100644
--- a/simctl.js
+++ b/simctl.js
@@ -23,9 +23,12 @@ THE SOFTWARE.
 */
 
 const shell = require('shelljs')
-const util = require('util')
 const SimCtlExtensions = require('./lib/simctl-extensions')
 
+const escapeShellArg = function (arg) {
+  return typeof arg === 'string' ? arg.replace(/(["`])/g, '\\$1') : arg;
+}
+
 exports = module.exports = {
 
   set noxpc (b) {
@@ -39,7 +42,7 @@ exports = module.exports = {
   extensions: SimCtlExtensions,
 
   check_prerequisites: function () {
-    const command = util.format('xcrun simctl help')
+    const command = `xcrun simctl help`
     const obj = shell.exec(command, { silent: true })
 
     if (obj.code !== 0) {
@@ -54,57 +57,57 @@ exports = module.exports = {
   },
 
   create: function (name, deviceTypeId, runtimeId) {
-    const command = util.format('xcrun simctl create "%s" "%s" "%s"', name, deviceTypeId, runtimeId)
+    const command = `xcrun simctl create "${escapeShellArg(name)}" "${escapeShellArg(deviceTypeId)}" "${escapeShellArg(runtimeId)}"`;
     return shell.exec(command)
   },
 
   del: function (device) {
-    const command = util.format('xcrun simctl delete "%s"', device)
+    const command = `xcrun simctl delete "${escapeShellArg(device)}"`
     return shell.exec(command)
   },
 
   erase: function (device) {
-    const command = util.format('xcrun simctl erase "%s"', device)
+    const command = `xcrun simctl erase "${escapeShellArg(device)}"`
     return shell.exec(command)
   },
 
   boot: function (device) {
-    const command = util.format('xcrun simctl boot "%s"', device)
+    const command = `xcrun simctl boot "${escapeShellArg(device)}"`
     return shell.exec(command)
   },
 
   shutdown: function (device) {
-    const command = util.format('xcrun simctl shutdown "%s"', device)
+    const command = `xcrun simctl shutdown "${escapeShellArg(device)}"`
     return shell.exec(command)
   },
 
   rename: function (device, name) {
-    const command = util.format('xcrun simctl rename "%s" "%s"', device, name)
+    const command = `xcrun simctl rename "${escapeShellArg(device)}" "${escapeShellArg(name)}"`
     return shell.exec(command)
   },
 
   getenv: function (device, variableName) {
-    const command = util.format('xcrun simctl getenv "%s" "%s"', device, variableName)
+    const command = `xcrun simctl getenv "${escapeShellArg(device)}" "${escapeShellArg(variableName)}"`
     return shell.exec(command)
   },
 
   openurl: function (device, url) {
-    const command = util.format('xcrun simctl openurl "%s" "%s"', device, url)
+    const command = `xcrun simctl openurl "${escapeShellArg(device)}" "${escapeShellArg(url)}"`
     return shell.exec(command)
   },
 
   addphoto: function (device, path) {
-    const command = util.format('xcrun simctl addphoto "%s" "%s"', device, path)
+    const command = `xcrun simctl addphoto "${escapeShellArg(device)}" "${escapeShellArg(path)}"`
     return shell.exec(command)
   },
 
   install: function (device, path) {
-    const command = util.format('xcrun simctl install "%s" "%s"', device, path)
+    const command = `xcrun simctl install "${escapeShellArg(device)}" "${escapeShellArg(path)}"`
     return shell.exec(command)
   },
 
   uninstall: function (device, appIdentifier) {
-    const command = util.format('xcrun simctl uninstall "%s" "%s"', device, appIdentifier)
+    const command = `xcrun simctl uninstall "${escapeShellArg(device)}" "${escapeShellArg(appIdentifier)}"`
     return shell.exec(command)
   },
 
@@ -121,8 +124,7 @@ exports = module.exports = {
       }).join(' ')
     }
 
-    const command = util.format('xcrun simctl launch %s "%s" "%s" %s',
-      waitFlag, device, appIdentifier, argvExpanded)
+    const command = `xcrun simctl launch ${waitFlag} "${escapeShellArg(device)}" "${escapeShellArg(appIdentifier)}" ${argvExpanded}`;
     return shell.exec(command)
   },
 
@@ -134,7 +136,7 @@ exports = module.exports = {
 
     let archFlag = ''
     if (arch) {
-      archFlag = util.format('--arch="%s"', arch)
+      archFlag = `--arch="${escapeShellArg(arch)}"', `
     }
 
     let argvExpanded = ''
@@ -144,8 +146,7 @@ exports = module.exports = {
       }).join(' ')
     }
 
-    const command = util.format('xcrun simctl spawn %s %s "%s" "%s" %s',
-      waitFlag, archFlag, device, pathToExecutable, argvExpanded)
+    const command = `xcrun simctl spawn ${waitFlag} ${archFlag} "${escapeShellArg(device)}" "${escapeShellArg(pathToExecutable)}" ${argvExpanded}`;
     return shell.exec(command)
   },
 
@@ -163,7 +164,7 @@ exports = module.exports = {
       sublist = 'pairs'
     }
 
-    const command = util.format('xcrun simctl list %s --json', sublist)
+    const command = `xcrun simctl list ${sublist} --json`
     const obj = shell.exec(command, { silent: options.silent })
 
     if (obj.code === 0) {
@@ -178,17 +179,17 @@ exports = module.exports = {
   },
 
   notify_post: function (device, notificationName) {
-    const command = util.format('xcrun simctl notify_post "%s" "%s"', device, notificationName)
+    const command = `xcrun simctl notify_post "${escapeShellArg(device)}" "${escapeShellArg(notificationName)}"}`;
     return shell.exec(command)
   },
 
   icloud_sync: function (device) {
-    const command = util.format('xcrun simctl icloud_sync "%s"', device)
+    const command = `xcrun simctl icloud_sync "${escapeShellArg(device)}"`
     return shell.exec(command)
   },
 
   help: function (subcommand) {
-    const command = util.format('xcrun simctl help "%s"', subcommand)
+    const command = `xcrun simctl help "${escapeShellArg(subcommand)}"`
     return shell.exec(command)
   }
 }