CVE-2023-28102: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: discordrb is an implementation of the Discord API using Ruby. In discordrb before commit `91e13043ffa` the `encoder.rb` file unsafely constructs a shell string using the file parameter, which can potentially leave clients of discordrb vulnerable to command injection. The library is not directly exploitable: the exploit requires that some client of the library calls the vulnerable method with user input. However, if unsafe input reaches the library method, then an attacker can execute arbitrary shell commands on the host machine. Full impact will depend on the permissions of the process running the `discordrb` library and will likely not be total system access. This issue has been addressed in code, but a new release of the `discordrb` gem has not been uploaded to rubygems. This issue is also tracked as `GHSL-2022-094`.
CWE description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
CVSS v3 base score: 9.6
Published: 2023-03-27T22:15Z
Repository: https://github.com/shardlab/discordrb
Language: Ruby
Files changed: lib/discordrb/voice/encoder.rb
Commit date: 2023-03-08 15:33:54 -0500
Commit message: fix: switch popen to use an array invocation

Diff: diff --git a/lib/discordrb/voice/encoder.rb b/lib/discordrb/voice/encoder.rb
index 807f1588..f288732a 100644
--- a/lib/discordrb/voice/encoder.rb
+++ b/lib/discordrb/voice/encoder.rb
@@ -75,28 +75,37 @@ module Discordrb::Voice
     # URLs, though encoding them is pretty slow - I recommend to make a stream of it and then use {#encode_io} instead.
     # @param file [String] The path or URL to encode.
     # @param options [String] ffmpeg options to pass after the -i flag
     # @return [IO] the audio, encoded as s16le PCM
     def encode_file(file, options = '')
-      command = "#{ffmpeg_command} -loglevel 0 -i \"#{file}\" #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
+      command = ffmpeg_command(input: file, options: options)
       IO.popen(command)
     end
 
     # Encodes an arbitrary IO audio stream using ffmpeg. Accepts pretty much any media format, even videos with audio
     # tracks. For a list of supported audio formats, see https://ffmpeg.org/general.html#Audio-Codecs.
     # @param io [IO] The stream to encode.
     # @param options [String] ffmpeg options to pass after the -i flag
     # @return [IO] the audio, encoded as s16le PCM
     def encode_io(io, options = '')
-      command = "#{ffmpeg_command} -loglevel 0 -i - #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
+      command = ffmpeg_command(options: options)
       IO.popen(command, in: io)
     end
 
     private
 
-    def ffmpeg_command
-      @use_avconv ? 'avconv' : 'ffmpeg'
+    def ffmpeg_command(input: '-', options: null)
+      [
+        @use_avconv ? 'avconv' : 'ffmpeg',
+        '-loglevel', '0',
+        '-i', input,
+        '-f', 's16le',
+        '-ar', '48000',
+        '-ac', '2',
+        'pipe:1',
+        filter_volume_argument,
+      ].concat(options.split).reject {|segment| segment.nil? || segment == '' }
     end
 
     def filter_volume_argument
       return '' if @filter_volume == 1
 

Vulnerable code:       command = "#{ffmpeg_command} -loglevel 0 -i \"#{file}\" #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
      command = "#{ffmpeg_command} -loglevel 0 -i - #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
    def ffmpeg_command
      @use_avconv ? 'avconv' : 'ffmpeg'
Fixed code:       command = ffmpeg_command(input: file, options: options)
      command = ffmpeg_command(options: options)
    def ffmpeg_command(input: '-', options: null)
      [
        @use_avconv ? 'avconv' : 'ffmpeg',
        '-loglevel', '0',
        '-i', input,
        '-f', 's16le',
        '-ar', '48000',
        '-ac', '2',
        'pipe:1',
        filter_volume_argument,
      ].concat(options.split).reject {|segment| segment.nil? || segment == '' }