Skip to content

Commit ac56f03

Browse files
author
Mikel Lindsaar
committed
Fix security vulnerability allowing command line exploit when using exim or sendmail from the command line
1 parent 47e288e commit ac56f03

File tree

7 files changed

+277
-67
lines changed

7 files changed

+277
-67
lines changed

Diff for: lib/mail.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ module Mail # :doc:
2929
require 'mail/core_extensions/nil'
3030
require 'mail/core_extensions/object'
3131
require 'mail/core_extensions/string'
32-
require 'mail/core_extensions/shellwords' unless String.new.respond_to?(:shellescape)
32+
require 'mail/core_extensions/shell_escape'
3333
require 'mail/core_extensions/smtp' if RUBY_VERSION < '1.9.3'
3434
require 'mail/indifferent_hash'
3535

Diff for: lib/mail/core_extensions/shell_escape.rb

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# encoding: utf-8
2+
3+
# The following is an adaptation of ruby 1.9.2's shellwords.rb file,
4+
# it is modified to include '+' in the allowed list to allow for
5+
# sendmail to accept email addresses as the sender with a + in them
6+
#
7+
module Mail
8+
module ShellEscape
9+
# Escapes a string so that it can be safely used in a Bourne shell
10+
# command line.
11+
#
12+
# Note that a resulted string should be used unquoted and is not
13+
# intended for use in double quotes nor in single quotes.
14+
#
15+
# open("| grep #{Shellwords.escape(pattern)} file") { |pipe|
16+
# # ...
17+
# }
18+
#
19+
# +String#shellescape+ is a shorthand for this function.
20+
#
21+
# open("| grep #{pattern.shellescape} file") { |pipe|
22+
# # ...
23+
# }
24+
#
25+
def escape_for_shell(str)
26+
# An empty argument will be skipped, so return empty quotes.
27+
return "''" if str.empty?
28+
29+
str = str.dup
30+
31+
# Process as a single byte sequence because not all shell
32+
# implementations are multibyte aware.
33+
str.gsub!(/([^A-Za-z0-9_\s\+\-.,:\/@\n])/n, "\\\\\\1")
34+
35+
# A LF cannot be escaped with a backslash because a backslash + LF
36+
# combo is regarded as line continuation and simply ignored.
37+
str.gsub!(/\n/, "'\n'")
38+
39+
return str
40+
end
41+
42+
module_function :escape_for_shell
43+
end
44+
end
45+
46+
class String
47+
# call-seq:
48+
# str.shellescape => string
49+
#
50+
# Escapes +str+ so that it can be safely used in a Bourne shell
51+
# command line. See +Shellwords::shellescape+ for details.
52+
#
53+
def escape_for_shell
54+
Mail::ShellEscape.escape_for_shell(self)
55+
end
56+
end

Diff for: lib/mail/core_extensions/shellwords.rb

-57
This file was deleted.

Diff for: lib/mail/network/delivery_methods/exim.rb

+38-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
11
module Mail
22

3+
# A delivery method implementation which sends via exim.
4+
#
5+
# To use this, first find out where the exim binary is on your computer,
6+
# if you are on a mac or unix box, it is usually in /usr/sbin/exim, this will
7+
# be your exim location.
8+
#
9+
# Mail.defaults do
10+
# delivery_method :exim
11+
# end
12+
#
13+
# Or if your exim binary is not at '/usr/sbin/exim'
14+
#
15+
# Mail.defaults do
16+
# delivery_method :exim, :location => '/absolute/path/to/your/exim'
17+
# end
18+
#
19+
# Then just deliver the email as normal:
20+
#
21+
# Mail.deliver do
22+
# to 'mikel@test.lindsaar.net'
23+
# from 'ada@test.lindsaar.net'
24+
# subject 'testing exim'
25+
# body 'testing exim'
26+
# end
27+
#
28+
# Or by calling deliver on a Mail message
29+
#
30+
# mail = Mail.new do
31+
# to 'mikel@test.lindsaar.net'
32+
# from 'ada@test.lindsaar.net'
33+
# subject 'testing exim'
34+
# body 'testing exim'
35+
# end
36+
#
37+
# mail.deliver!
338
class Exim < Sendmail
439

5-
def deliver!(mail)
6-
envelope_from = mail.return_path || mail.sender || mail.from_addrs.first
7-
return_path = "-f \"#{envelope_from.to_s.shellescape}\"" if envelope_from
8-
arguments = [settings[:arguments], return_path].compact.join(" ")
9-
self.class.call(settings[:location], arguments, mail)
40+
def initialize(values)
41+
self.settings = { :location => '/usr/sbin/exim',
42+
:arguments => '-i -t' }.merge(values)
1043
end
1144

1245
def self.call(path, arguments, mail)

Diff for: lib/mail/network/delivery_methods/sendmail.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@ def initialize(values)
4545

4646
def deliver!(mail)
4747
envelope_from = mail.return_path || mail.sender || mail.from_addrs.first
48-
return_path = "-f \"#{envelope_from.to_s.gsub('"', '\"')}\"" if envelope_from
48+
return_path = "-f " + '"' + envelope_from.escape_for_shell + '"' if envelope_from
4949

5050
arguments = [settings[:arguments], return_path].compact.join(" ")
5151

52-
Sendmail.call(settings[:location], arguments, mail.destinations.collect(&:shellescape).join(" "), mail)
52+
self.class.call(settings[:location], arguments, mail.destinations.collect(&:shellescape).join(" "), mail)
5353
end
5454

55-
def Sendmail.call(path, arguments, destinations, mail)
55+
def self.call(path, arguments, destinations, mail)
5656
IO.popen("#{path} #{arguments} #{destinations}", "w+") do |io|
5757
io.puts mail.encoded.to_lf
5858
io.flush

Diff for: spec/mail/network/delivery_methods/exim_spec.rb

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# encoding: utf-8
2+
require 'spec_helper'
3+
4+
describe "exim delivery agent" do
5+
6+
before(:each) do
7+
# Reset all defaults back to original state
8+
Mail.defaults do
9+
delivery_method :smtp, { :address => "localhost",
10+
:port => 25,
11+
:domain => 'localhost.localdomain',
12+
:user_name => nil,
13+
:password => nil,
14+
:authentication => nil,
15+
:enable_starttls_auto => true }
16+
end
17+
end
18+
19+
it "should send an email using exim" do
20+
Mail.defaults do
21+
delivery_method :exim
22+
end
23+
24+
mail = Mail.new do
25+
from 'roger@test.lindsaar.net'
26+
to 'marcel@test.lindsaar.net, bob@test.lindsaar.net'
27+
subject 'invalid RFC2822'
28+
end
29+
30+
Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
31+
'-i -t -f "roger@test.lindsaar.net"',
32+
'marcel@test.lindsaar.net bob@test.lindsaar.net',
33+
mail)
34+
mail.deliver!
35+
end
36+
37+
describe "return path" do
38+
39+
it "should send an email with a return-path using exim" do
40+
Mail.defaults do
41+
delivery_method :exim
42+
end
43+
44+
mail = Mail.new do
45+
to "to@test.lindsaar.net"
46+
from "from@test.lindsaar.net"
47+
sender "sender@test.lindsaar.net"
48+
subject "Can't set the return-path"
49+
return_path "return@test.lindsaar.net"
50+
message_id "<1234@test.lindsaar.net>"
51+
body "body"
52+
end
53+
54+
Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
55+
'-i -t -f "return@test.lindsaar.net"',
56+
'to@test.lindsaar.net',
57+
mail)
58+
59+
mail.deliver
60+
61+
end
62+
63+
it "should use the sender address is no return path is specified" do
64+
Mail.defaults do
65+
delivery_method :exim
66+
end
67+
68+
mail = Mail.new do
69+
to "to@test.lindsaar.net"
70+
from "from@test.lindsaar.net"
71+
sender "sender@test.lindsaar.net"
72+
subject "Can't set the return-path"
73+
message_id "<1234@test.lindsaar.net>"
74+
body "body"
75+
end
76+
77+
Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
78+
'-i -t -f "sender@test.lindsaar.net"',
79+
'to@test.lindsaar.net',
80+
mail)
81+
82+
mail.deliver
83+
end
84+
85+
it "should use the from address is no return path or sender are specified" do
86+
Mail.defaults do
87+
delivery_method :exim
88+
end
89+
90+
mail = Mail.new do
91+
to "to@test.lindsaar.net"
92+
from "from@test.lindsaar.net"
93+
subject "Can't set the return-path"
94+
message_id "<1234@test.lindsaar.net>"
95+
body "body"
96+
end
97+
98+
Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
99+
'-i -t -f "from@test.lindsaar.net"',
100+
'to@test.lindsaar.net',
101+
mail)
102+
mail.deliver
103+
end
104+
105+
it "should escape the return path address" do
106+
Mail.defaults do
107+
delivery_method :exim
108+
end
109+
110+
mail = Mail.new do
111+
to 'to@test.lindsaar.net'
112+
from '"from+suffix test"@test.lindsaar.net'
113+
subject 'Can\'t set the return-path'
114+
message_id '<1234@test.lindsaar.net>'
115+
body 'body'
116+
end
117+
118+
Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
119+
'-i -t -f "\"from+suffix test\"@test.lindsaar.net"',
120+
'to@test.lindsaar.net',
121+
mail)
122+
mail.deliver
123+
end
124+
end
125+
126+
it "should still send an email if the settings have been set to nil" do
127+
Mail.defaults do
128+
delivery_method :exim, :arguments => nil
129+
end
130+
131+
mail = Mail.new do
132+
from 'from@test.lindsaar.net'
133+
to 'marcel@test.lindsaar.net, bob@test.lindsaar.net'
134+
subject 'invalid RFC2822'
135+
end
136+
137+
Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
138+
'-f "from@test.lindsaar.net"',
139+
'marcel@test.lindsaar.net bob@test.lindsaar.net',
140+
mail)
141+
mail.deliver!
142+
end
143+
144+
it "should escape evil haxxor attemptes" do
145+
Mail.defaults do
146+
delivery_method :exim, :arguments => nil
147+
end
148+
149+
mail = Mail.new do
150+
from '"foo\";touch /tmp/PWNED;\""@blah.com'
151+
to 'marcel@test.lindsaar.net'
152+
subject 'invalid RFC2822'
153+
end
154+
155+
Mail::Exim.should_receive(:call).with('/usr/sbin/exim',
156+
"-f \"\\\"foo\\\\\\\"\\;touch /tmp/PWNED\\;\\\\\\\"\\\"@blah.com\"",
157+
'marcel@test.lindsaar.net',
158+
mail)
159+
mail.deliver!
160+
end
161+
end

Diff for: spec/mail/network/delivery_methods/sendmail_spec.rb

+18-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@
123123
end
124124
end
125125

126-
127126
it "should still send an email if the settings have been set to nil" do
128127
Mail.defaults do
129128
delivery_method :sendmail, :arguments => nil
@@ -141,4 +140,22 @@
141140
mail)
142141
mail.deliver!
143142
end
143+
144+
it "should escape evil haxxor attemptes" do
145+
Mail.defaults do
146+
delivery_method :sendmail, :arguments => nil
147+
end
148+
149+
mail = Mail.new do
150+
from '"foo\";touch /tmp/PWNED;\""@blah.com'
151+
to 'marcel@test.lindsaar.net'
152+
subject 'invalid RFC2822'
153+
end
154+
155+
Mail::Sendmail.should_receive(:call).with('/usr/sbin/sendmail',
156+
"-f \"\\\"foo\\\\\\\"\\;touch /tmp/PWNED\\;\\\\\\\"\\\"@blah.com\"",
157+
'marcel@test.lindsaar.net',
158+
mail)
159+
mail.deliver!
160+
end
144161
end

0 commit comments

Comments
 (0)