Skip to content
Browse files

Fix security vulnerability allowing command line exploit when using e…

…xim or sendmail from the command line
  • Loading branch information...
1 parent 47e288e commit ac56f03bdfc30b379aeecd4ff317d08fdaa328c2 @mikel committed Mar 6, 2012
View
2 lib/mail.rb
@@ -29,7 +29,7 @@ module Mail # :doc:
require 'mail/core_extensions/nil'
require 'mail/core_extensions/object'
require 'mail/core_extensions/string'
- require 'mail/core_extensions/shellwords' unless String.new.respond_to?(:shellescape)
+ require 'mail/core_extensions/shell_escape'
require 'mail/core_extensions/smtp' if RUBY_VERSION < '1.9.3'
require 'mail/indifferent_hash'
View
56 lib/mail/core_extensions/shell_escape.rb
@@ -0,0 +1,56 @@
+# encoding: utf-8
+
+# The following is an adaptation of ruby 1.9.2's shellwords.rb file,
+# it is modified to include '+' in the allowed list to allow for
+# sendmail to accept email addresses as the sender with a + in them
+#
+module Mail
+ module ShellEscape
+ # Escapes a string so that it can be safely used in a Bourne shell
+ # command line.
+ #
+ # Note that a resulted string should be used unquoted and is not
+ # intended for use in double quotes nor in single quotes.
+ #
+ # open("| grep #{Shellwords.escape(pattern)} file") { |pipe|
+ # # ...
+ # }
+ #
+ # +String#shellescape+ is a shorthand for this function.
+ #
+ # open("| grep #{pattern.shellescape} file") { |pipe|
+ # # ...
+ # }
+ #
+ def escape_for_shell(str)
+ # An empty argument will be skipped, so return empty quotes.
+ return "''" if str.empty?
+
+ str = str.dup
+
+ # Process as a single byte sequence because not all shell
+ # implementations are multibyte aware.
+ str.gsub!(/([^A-Za-z0-9_\s\+\-.,:\/@\n])/n, "\\\\\\1")
+
+ # A LF cannot be escaped with a backslash because a backslash + LF
+ # combo is regarded as line continuation and simply ignored.
+ str.gsub!(/\n/, "'\n'")
+
+ return str
+ end
+
+ module_function :escape_for_shell
+ end
+end
+
+class String
+ # call-seq:
+ # str.shellescape => string
+ #
+ # Escapes +str+ so that it can be safely used in a Bourne shell
+ # command line. See +Shellwords::shellescape+ for details.
+ #
+ def escape_for_shell
+ Mail::ShellEscape.escape_for_shell(self)
+ end
+end
View
57 lib/mail/core_extensions/shellwords.rb
@@ -1,57 +0,0 @@
-# encoding: utf-8
-
-# The following is imported from ruby 1.9.2 shellwords.rb
-#
-module Shellwords
- # Escapes a string so that it can be safely used in a Bourne shell
- # command line.
- #
- # Note that a resulted string should be used unquoted and is not
- # intended for use in double quotes nor in single quotes.
- #
- # open("| grep #{Shellwords.escape(pattern)} file") { |pipe|
- # # ...
- # }
- #
- # +String#shellescape+ is a shorthand for this function.
- #
- # open("| grep #{pattern.shellescape} file") { |pipe|
- # # ...
- # }
- #
- def shellescape(str)
- # An empty argument will be skipped, so return empty quotes.
- return "''" if str.empty?
-
- str = str.dup
-
- # Process as a single byte sequence because not all shell
- # implementations are multibyte aware.
- str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
-
- # A LF cannot be escaped with a backslash because a backslash + LF
- # combo is regarded as line continuation and simply ignored.
- str.gsub!(/\n/, "'\n'")
-
- return str
- end
-
- module_function :shellescape
-
- class << self
- alias escape shellescape
- end
-
-end
-
-class String
- # call-seq:
- # str.shellescape => string
- #
- # Escapes +str+ so that it can be safely used in a Bourne shell
- # command line. See +Shellwords::shellescape+ for details.
- #
- def shellescape
- Shellwords.escape(self)
- end
-end
View
43 lib/mail/network/delivery_methods/exim.rb
@@ -1,12 +1,45 @@
module Mail
+ # A delivery method implementation which sends via exim.
+ #
+ # To use this, first find out where the exim binary is on your computer,
+ # if you are on a mac or unix box, it is usually in /usr/sbin/exim, this will
+ # be your exim location.
+ #
+ # Mail.defaults do
+ # delivery_method :exim
+ # end
+ #
+ # Or if your exim binary is not at '/usr/sbin/exim'
+ #
+ # Mail.defaults do
+ # delivery_method :exim, :location => '/absolute/path/to/your/exim'
+ # end
+ #
+ # Then just deliver the email as normal:
+ #
+ # Mail.deliver do
+ # to 'mikel@test.lindsaar.net'
+ # from 'ada@test.lindsaar.net'
+ # subject 'testing exim'
+ # body 'testing exim'
+ # end
+ #
+ # Or by calling deliver on a Mail message
+ #
+ # mail = Mail.new do
+ # to 'mikel@test.lindsaar.net'
+ # from 'ada@test.lindsaar.net'
+ # subject 'testing exim'
+ # body 'testing exim'
+ # end
+ #
+ # mail.deliver!
class Exim < Sendmail
- def deliver!(mail)
- envelope_from = mail.return_path || mail.sender || mail.from_addrs.first
- return_path = "-f \"#{envelope_from.to_s.shellescape}\"" if envelope_from
- arguments = [settings[:arguments], return_path].compact.join(" ")
- self.class.call(settings[:location], arguments, mail)
+ def initialize(values)
+ self.settings = { :location => '/usr/sbin/exim',
+ :arguments => '-i -t' }.merge(values)
end
def self.call(path, arguments, mail)
View
6 lib/mail/network/delivery_methods/sendmail.rb
@@ -45,14 +45,14 @@ def initialize(values)
def deliver!(mail)
envelope_from = mail.return_path || mail.sender || mail.from_addrs.first
- return_path = "-f \"#{envelope_from.to_s.gsub('"', '\"')}\"" if envelope_from
+ return_path = "-f " + '"' + envelope_from.escape_for_shell + '"' if envelope_from
arguments = [settings[:arguments], return_path].compact.join(" ")
- Sendmail.call(settings[:location], arguments, mail.destinations.collect(&:shellescape).join(" "), mail)
+ self.class.call(settings[:location], arguments, mail.destinations.collect(&:shellescape).join(" "), mail)
end
- def Sendmail.call(path, arguments, destinations, mail)
+ def self.call(path, arguments, destinations, mail)
IO.popen("#{path} #{arguments} #{destinations}", "w+") do |io|
io.puts mail.encoded.to_lf
io.flush
View
161 spec/mail/network/delivery_methods/exim_spec.rb
@@ -0,0 +1,161 @@
+# encoding: utf-8
+require 'spec_helper'
+
+describe "exim delivery agent" do
+
+ before(:each) do
+ # Reset all defaults back to original state
+ Mail.defaults do
+ delivery_method :smtp, { :address => "localhost",
+ :port => 25,
+ :domain => 'localhost.localdomain',
+ :user_name => nil,
+ :password => nil,
+ :authentication => nil,
+ :enable_starttls_auto => true }
+ end
+ end
+
+ it "should send an email using exim" do
+ Mail.defaults do
+ delivery_method :exim
+ end
+
+ mail = Mail.new do
+ from 'roger@test.lindsaar.net'
+ to 'marcel@test.lindsaar.net, bob@test.lindsaar.net'
+ subject 'invalid RFC2822'
+ end
+
+ Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
+ '-i -t -f "roger@test.lindsaar.net"',
+ 'marcel@test.lindsaar.net bob@test.lindsaar.net',
+ mail)
+ mail.deliver!
+ end
+
+ describe "return path" do
+
+ it "should send an email with a return-path using exim" do
+ Mail.defaults do
+ delivery_method :exim
+ end
+
+ mail = Mail.new do
+ to "to@test.lindsaar.net"
+ from "from@test.lindsaar.net"
+ sender "sender@test.lindsaar.net"
+ subject "Can't set the return-path"
+ return_path "return@test.lindsaar.net"
+ message_id "<1234@test.lindsaar.net>"
+ body "body"
+ end
+
+ Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
+ '-i -t -f "return@test.lindsaar.net"',
+ 'to@test.lindsaar.net',
+ mail)
+
+ mail.deliver
+
+ end
+
+ it "should use the sender address is no return path is specified" do
+ Mail.defaults do
+ delivery_method :exim
+ end
+
+ mail = Mail.new do
+ to "to@test.lindsaar.net"
+ from "from@test.lindsaar.net"
+ sender "sender@test.lindsaar.net"
+ subject "Can't set the return-path"
+ message_id "<1234@test.lindsaar.net>"
+ body "body"
+ end
+
+ Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
+ '-i -t -f "sender@test.lindsaar.net"',
+ 'to@test.lindsaar.net',
+ mail)
+
+ mail.deliver
+ end
+
+ it "should use the from address is no return path or sender are specified" do
+ Mail.defaults do
+ delivery_method :exim
+ end
+
+ mail = Mail.new do
+ to "to@test.lindsaar.net"
+ from "from@test.lindsaar.net"
+ subject "Can't set the return-path"
+ message_id "<1234@test.lindsaar.net>"
+ body "body"
+ end
+
+ Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
+ '-i -t -f "from@test.lindsaar.net"',
+ 'to@test.lindsaar.net',
+ mail)
+ mail.deliver
+ end
+
+ it "should escape the return path address" do
+ Mail.defaults do
+ delivery_method :exim
+ end
+
+ mail = Mail.new do
+ to 'to@test.lindsaar.net'
+ from '"from+suffix test"@test.lindsaar.net'
+ subject 'Can\'t set the return-path'
+ message_id '<1234@test.lindsaar.net>'
+ body 'body'
+ end
+
+ Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
+ '-i -t -f "\"from+suffix test\"@test.lindsaar.net"',
+ 'to@test.lindsaar.net',
+ mail)
+ mail.deliver
+ end
+ end
+
+ it "should still send an email if the settings have been set to nil" do
+ Mail.defaults do
+ delivery_method :exim, :arguments => nil
+ end
+
+ mail = Mail.new do
+ from 'from@test.lindsaar.net'
+ to 'marcel@test.lindsaar.net, bob@test.lindsaar.net'
+ subject 'invalid RFC2822'
+ end
+
+ Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
+ '-f "from@test.lindsaar.net"',
+ 'marcel@test.lindsaar.net bob@test.lindsaar.net',
+ mail)
+ mail.deliver!
+ end
+
+ it "should escape evil haxxor attemptes" do
+ Mail.defaults do
+ delivery_method :exim, :arguments => nil
+ end
+
+ mail = Mail.new do
+ from '"foo\";touch /tmp/PWNED;\""@blah.com'
+ to 'marcel@test.lindsaar.net'
+ subject 'invalid RFC2822'
+ end
+
+ Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
+ "-f \"\\\"foo\\\\\\\"\\;touch /tmp/PWNED\\;\\\\\\\"\\\"@blah.com\"",
+ 'marcel@test.lindsaar.net',
+ mail)
+ mail.deliver!
+ end
+end
View
19 spec/mail/network/delivery_methods/sendmail_spec.rb
@@ -123,7 +123,6 @@
end
end
-
it "should still send an email if the settings have been set to nil" do
Mail.defaults do
delivery_method :sendmail, :arguments => nil
@@ -141,4 +140,22 @@
mail)
mail.deliver!
end
+
+ it "should escape evil haxxor attemptes" do
+ Mail.defaults do
+ delivery_method :sendmail, :arguments => nil
+ end
+
+ mail = Mail.new do
+ from '"foo\";touch /tmp/PWNED;\""@blah.com'
+ to 'marcel@test.lindsaar.net'
+ subject 'invalid RFC2822'
+ end
+
+ Mail::Sendmail.should_receive(:call).with('/usr/sbin/sendmail',
+ "-f \"\\\"foo\\\\\\\"\\;touch /tmp/PWNED\\;\\\\\\\"\\\"@blah.com\"",
+ 'marcel@test.lindsaar.net',
+ mail)
+ mail.deliver!
+ end
end

0 comments on commit ac56f03

Please sign in to comment.
Something went wrong with that request. Please try again.