sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit a5acc24937320456e244699b8551a9164641f89b
parent adeab88fe422abc3576e2f3421bdf48b1dd71d5e
Author: Gaute Hope <eg@gaute.vetsj.com>
Date:   Mon, 28 Oct 2013 12:11:58 +0100

security: shellwords escape attachment file names to prevent remote code injection

Merge peristent temp files and fix quotation of shellwords escaped string

s|returnded|returned|

Diffstat:
M doc/Hooks.txt | 2 +-
M lib/sup/message_chunks.rb | 59 +++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 48 insertions(+), 13 deletions(-)
diff --git a/doc/Hooks.txt b/doc/Hooks.txt
@@ -54,7 +54,7 @@ mime-decode:
   unless sibling_types.member? "text/plain"
     case content_type
     when "text/html"
-      `/usr/bin/w3m -dump -T #{content_type} '#{Shellwords.escape filename}'`
+      `/usr/bin/w3m -dump -T #{content_type} #{Shellwords.escape filename}`
     end
   end
 
diff --git a/lib/sup/message_chunks.rb b/lib/sup/message_chunks.rb
@@ -1,5 +1,6 @@
 require 'tempfile'
 require 'rbconfig'
+require 'shellwords'
 
 ## Here we define all the "chunks" that a message is parsed
 ## into. Chunks are used by ThreadViewMode to render a message. Chunks
@@ -59,6 +60,8 @@ end
 module Redwood
 module Chunk
   class Attachment
+    ## please see note in write_to_disk on important usage
+    ## of quotes to avoid remote command injection.
     HookManager.register "mime-decode", <<EOS
 Decodes a MIME attachment into text form. The text will be displayed
 directly in Sup. For attachments that you wish to use a separate program
@@ -75,6 +78,9 @@ Return value:
   The decoded text of the attachment, or nil if not decoded.
 EOS
 
+
+    ## please see note in write_to_disk on important usage
+    ## of quotes to avoid remote command injection.
     HookManager.register "mime-view", <<EOS
 Views a non-text MIME attachment. This hook allows you to run
 third-party programs for attachments that require such a thing (e.g.
@@ -100,6 +106,11 @@ EOS
     attr_reader :content_type, :filename, :lines, :raw_content
     bool_reader :quotable
 
+    ## store tempfile objects as class variables so that they
+    ## are not removed when the viewing process returns. they
+    ## should be garbage collected when the class variable is removed.
+    @@view_tempfiles = []
+
     def initialize content_type, filename, encoded_content, sibling_types
       @content_type = content_type.downcase
       @filename = filename
@@ -116,6 +127,8 @@ EOS
       when /^text\/plain\b/
         @raw_content
       else
+        ## please see note in write_to_disk on important usage
+        ## of quotes to avoid remote command injection.
         HookManager.run "mime-decode", :content_type => content_type,
                         :filename => lambda { write_to_disk },
                         :charset => encoded_content.charset,
@@ -125,7 +138,13 @@ EOS
       @lines = nil
       if text
         text = text.transcode(encoded_content.charset || $encoding, text.encoding)
-        @lines = text.gsub("\r\n", "\n").gsub(/\t/, "        ").gsub(/\r/, "").split("\n")
+        begin
+          @lines = text.gsub("\r\n", "\n").gsub(/\t/, "        ").gsub(/\r/, "").split("\n")
+        rescue Encoding::CompatibilityError
+          @lines = text.fix_encoding!.gsub("\r\n", "\n").gsub(/\t/, "        ").gsub(/\r/, "").split("\n")
+          debug "error while decoding message text, falling back to default encoding, expect errors in encoding: #{text.fix_encoding!}"
+        end
+
         @quotable = true
       end
     end
@@ -147,11 +166,13 @@ EOS
     def initial_state; :open end
     def viewable?; @lines.nil? end
     def view_default! path
+      ## please see note in write_to_disk on important usage
+      ## of quotes to avoid remote command injection.
       case RbConfig::CONFIG['arch']
         when /darwin/
-          cmd = "open '#{path}'"
+          cmd = "open #{path}"
         else
-          cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}'"
+          cmd = "/usr/bin/run-mailcap --action=view #{@content_type}:#{path}"
       end
       debug "running: #{cmd.inspect}"
       BufferManager.shell_out(cmd)
@@ -159,17 +180,31 @@ EOS
     end
 
     def view!
-      path = write_to_disk
-      ret = HookManager.run "mime-view", :content_type => @content_type,
-                                         :filename => path
-      ret || view_default!(path)
+      ## please see note in write_to_disk on important usage
+      ## of quotes to avoid remote command injection.
+      write_to_disk do |file|
+
+        @@view_tempfiles.push file # make sure the tempfile is not garbage collected before sup stops
+
+        ret = HookManager.run "mime-view", :content_type => @content_type,
+                                           :filename => file.path
+        ret || view_default!(file.path)
+      end
     end
 
+    ## note that the path returned from write_to_disk is
+    ## Shellwords.escaped and is intended to be used without single
+    ## or double quotes. the use of either opens sup up for remote
+    ## code injection in the file name.
     def write_to_disk
-      file = Tempfile.new(["sup", @filename.gsub("/", "_") || "sup-attachment"])
-      file.print @raw_content
-      file.close
-      file.path
+      begin
+        file = Tempfile.new(["sup", Shellwords.escape(@filename.gsub("/", "_")) || "sup-attachment"])
+        file.print @raw_content
+        yield file if block_given?
+        return file.path
+      ensure
+        file.close
+      end
     end
 
     ## used when viewing the attachment as text
@@ -229,7 +264,7 @@ EOS
   class EnclosedMessage
     attr_reader :lines
     def initialize from, to, cc, date, subj
-      @from = from ? "unknown sender" : from.full_adress
+      @from = from ? "unknown sender" : from.full_address
       @to = to ? "" : to.map { |p| p.full_address }.join(", ")
       @cc = cc ? "" : cc.map { |p| p.full_address }.join(", ")
       if date