55require "rainbow"
66require "active_support"
77require "active_support/core_ext/string"
8+ require "shellwords"
89
910# rubocop:disable Metrics/ModuleLength
1011module ReactOnRails
@@ -293,6 +294,42 @@ def self.detect_package_manager
293294 manager || :yarn # Default to yarn if no detection succeeds
294295 end
295296
297+ # Validates package_name input to prevent command injection
298+ #
299+ # @param package_name [String] The package name to validate
300+ # @raise [ReactOnRails::Error] if package_name contains potentially unsafe characters
301+ private_class_method def self . validate_package_name! ( package_name )
302+ raise ReactOnRails ::Error , "package_name cannot be nil" if package_name . nil?
303+ raise ReactOnRails ::Error , "package_name cannot be empty" if package_name . to_s . strip . empty?
304+
305+ # Allow valid npm package names: alphanumeric, hyphens, underscores, dots, slashes (for scoped packages)
306+ # See: https://github.com/npm/validate-npm-package-name
307+ return if package_name . match? ( %r{\A [@a-z0-9][a-z0-9._/-]*\z }i )
308+
309+ raise ReactOnRails ::Error , "Invalid package name: #{ package_name . inspect } . " \
310+ "Package names must contain only alphanumeric characters, " \
311+ "hyphens, underscores, dots, and slashes (for scoped packages)."
312+ end
313+
314+ # Validates package_name and version inputs to prevent command injection
315+ #
316+ # @param package_name [String] The package name to validate
317+ # @param version [String] The version to validate
318+ # @raise [ReactOnRails::Error] if inputs contain potentially unsafe characters
319+ private_class_method def self . validate_package_command_inputs! ( package_name , version )
320+ validate_package_name! ( package_name )
321+
322+ raise ReactOnRails ::Error , "version cannot be nil" if version . nil?
323+ raise ReactOnRails ::Error , "version cannot be empty" if version . to_s . strip . empty?
324+
325+ # Allow valid semver versions and common npm version patterns
326+ # This allows: 1.2.3, 1.2.3-beta.1, 1.2.3-alpha, etc.
327+ return if version . match? ( /\A [a-z0-9][a-z0-9._-]*\z /i )
328+
329+ raise ReactOnRails ::Error , "Invalid version: #{ version . inspect } . " \
330+ "Versions must contain only alphanumeric characters, dots, hyphens, and underscores."
331+ end
332+
296333 private_class_method def self . detect_package_manager_from_package_json
297334 package_json_path = File . join ( Rails . root , ReactOnRails . configuration . node_modules_location , "package.json" )
298335 return nil unless File . exist? ( package_json_path )
@@ -325,17 +362,21 @@ def self.detect_package_manager
325362 # @param version [String] The exact version to install
326363 # @return [String] The command to run (e.g., "yarn add [email protected] --exact") 327364 def self . package_manager_install_exact_command ( package_name , version )
365+ validate_package_command_inputs! ( package_name , version )
366+
328367 manager = detect_package_manager
368+ # Escape shell arguments to prevent command injection
369+ safe_package = Shellwords . escape ( "#{ package_name } @#{ version } " )
329370
330371 case manager
331372 when :pnpm
332- "pnpm add #{ package_name } @ #{ version } --save-exact"
373+ "pnpm add #{ safe_package } --save-exact"
333374 when :bun
334- "bun add #{ package_name } @ #{ version } --exact"
375+ "bun add #{ safe_package } --exact"
335376 when :npm
336- "npm install #{ package_name } @ #{ version } --save-exact"
377+ "npm install #{ safe_package } --save-exact"
337378 else # :yarn or unknown, default to yarn
338- "yarn add #{ package_name } @ #{ version } --exact"
379+ "yarn add #{ safe_package } --exact"
339380 end
340381 end
341382
@@ -344,17 +385,21 @@ def self.package_manager_install_exact_command(package_name, version)
344385 # @param package_name [String] The name of the package to remove
345386 # @return [String] The command to run (e.g., "yarn remove react-on-rails")
346387 def self . package_manager_remove_command ( package_name )
388+ validate_package_name! ( package_name )
389+
347390 manager = detect_package_manager
391+ # Escape shell arguments to prevent command injection
392+ safe_package = Shellwords . escape ( package_name )
348393
349394 case manager
350395 when :pnpm
351- "pnpm remove #{ package_name } "
396+ "pnpm remove #{ safe_package } "
352397 when :bun
353- "bun remove #{ package_name } "
398+ "bun remove #{ safe_package } "
354399 when :npm
355- "npm uninstall #{ package_name } "
400+ "npm uninstall #{ safe_package } "
356401 else # :yarn or unknown, default to yarn
357- "yarn remove #{ package_name } "
402+ "yarn remove #{ safe_package } "
358403 end
359404 end
360405
0 commit comments