Skip to content

Commit 31c6ca4

Browse files
committed
SMTP security: prevent command injection via To/From addresses
Validate addresses passed as SMTP command arguments to prevent injection of other SMTP commands. Disallow line breaks and very long addresses which may cause overflows on some old SMTP servers. Ruby 2.4 Net::SMTP already disallows addresses that contain newlines. Enforce this validation in Mail to cover older Ruby versions and other SMTP implementations that don't validate input. SMTP injection whitepaper: http://www.mbsd.jp/Whitepaper/smtpi.pdf Ruby security report: https://hackerone.com/reports/137631 OSVDB entry: https://rubysec.com/advisories/mail-OSVDB-131677
1 parent 5bcf5df commit 31c6ca4

File tree

13 files changed

+137
-54
lines changed

13 files changed

+137
-54
lines changed

CHANGELOG.rdoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
== HEAD
22

3+
Security:
4+
* #1097 – SMTP security: prevent command injection via To/From addresses. (jeremy)
5+
36
Features:
47
* #804 - Configurable SMTP open_timeout and read_timeout. (ankane)
58
* #853 - `Mail::Message#set_sort_order` overrides the default message part sort order. (rafbm)

lib/mail/check_delivery_params.rb

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,58 @@
11
# frozen_string_literal: true
22
module Mail
3-
module CheckDeliveryParams
4-
def check_delivery_params(mail)
5-
if Utilities.blank?(mail.smtp_envelope_from)
6-
raise ArgumentError.new('An SMTP From address is required to send a message. Set the message smtp_envelope_from, return_path, sender, or from address.')
3+
module CheckDeliveryParams #:nodoc:
4+
class << self
5+
def check(mail)
6+
[ check_from(mail.smtp_envelope_from),
7+
check_to(mail.smtp_envelope_to),
8+
check_message(mail) ]
79
end
810

9-
if Utilities.blank?(mail.smtp_envelope_to)
10-
raise ArgumentError.new('An SMTP To address is required to send a message. Set the message smtp_envelope_to, to, cc, or bcc address.')
11+
def check_from(addr)
12+
if Utilities.blank?(addr)
13+
raise ArgumentError, "SMTP From address may not be blank: #{addr.inspect}"
14+
end
15+
16+
check_addr 'From', addr
17+
end
18+
19+
def check_to(addrs)
20+
if Utilities.blank?(addrs)
21+
raise ArgumentError, "SMTP To address may not be blank: #{addrs.inspect}"
22+
end
23+
24+
Array(addrs).map do |addr|
25+
check_addr 'To', addr
26+
end
1127
end
1228

13-
message = mail.encoded if mail.respond_to?(:encoded)
14-
if Utilities.blank?(message)
15-
raise ArgumentError.new('An encoded message is required to send an email')
29+
def check_addr(addr_name, addr)
30+
validate_smtp_addr addr do |error_message|
31+
raise ArgumentError, "SMTP #{addr_name} address #{error_message}: #{addr.inspect}"
32+
end
1633
end
1734

18-
[mail.smtp_envelope_from, mail.smtp_envelope_to, message]
35+
def validate_smtp_addr(addr)
36+
if addr.bytesize > 2048
37+
yield 'may not exceed 2kB'
38+
end
39+
40+
if /[\r\n]/ =~ addr
41+
yield 'may not contain CR or LF line breaks'
42+
end
43+
44+
addr
45+
end
46+
47+
def check_message(message)
48+
message = message.encoded if message.respond_to?(:encoded)
49+
50+
if Utilities.blank?(message)
51+
raise ArgumentError, 'An encoded message is required to send an email'
52+
end
53+
54+
message
55+
end
1956
end
2057
end
2158
end

lib/mail/network/delivery_methods/file_delivery.rb

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
require 'mail/check_delivery_params'
33

44
module Mail
5-
65
# FileDelivery class delivers emails into multiple files based on the destination
76
# address. Each file is appended to if it already exists.
87
#
@@ -14,22 +13,20 @@ module Mail
1413
# Make sure the path you specify with :location is writable by the Ruby process
1514
# running Mail.
1615
class FileDelivery
17-
include Mail::CheckDeliveryParams
18-
1916
if RUBY_VERSION >= '1.9.1'
2017
require 'fileutils'
2118
else
2219
require 'ftools'
2320
end
2421

22+
attr_accessor :settings
23+
2524
def initialize(values)
2625
self.settings = { :location => './mails' }.merge!(values)
2726
end
28-
29-
attr_accessor :settings
30-
27+
3128
def deliver!(mail)
32-
check_delivery_params(mail)
29+
Mail::CheckDeliveryParams.check(mail)
3330

3431
if ::File.respond_to?(:makedirs)
3532
::File.makedirs settings[:location]
@@ -41,6 +38,5 @@ def deliver!(mail)
4138
::File.open(::File.join(settings[:location], File.basename(to.to_s)), 'a') { |f| "#{f.write(mail.encoded)}\r\n\r\n" }
4239
end
4340
end
44-
4541
end
4642
end

lib/mail/network/delivery_methods/sendmail.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,15 @@ module Mail
3838
#
3939
# mail.deliver!
4040
class Sendmail
41-
include Mail::CheckDeliveryParams
41+
attr_accessor :settings
4242

4343
def initialize(values)
4444
self.settings = { :location => '/usr/sbin/sendmail',
4545
:arguments => '-i' }.merge(values)
4646
end
4747

48-
attr_accessor :settings
49-
5048
def deliver!(mail)
51-
smtp_from, smtp_to, message = check_delivery_params(mail)
49+
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)
5250

5351
from = "-f #{self.class.shellquote(smtp_from)}"
5452
to = smtp_to.map { |_to| self.class.shellquote(_to) }.join(' ')

lib/mail/network/delivery_methods/smtp.rb

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ module Mail
7474
#
7575
# mail.deliver!
7676
class SMTP
77-
include Mail::CheckDeliveryParams
77+
attr_accessor :settings
7878

7979
def initialize(values)
8080
self.settings = { :address => "localhost",
@@ -93,12 +93,10 @@ def initialize(values)
9393
}.merge!(values)
9494
end
9595

96-
attr_accessor :settings
97-
9896
# Send the message via SMTP.
9997
# The from and to attributes are optional. If not set, they are retrieve from the Message.
10098
def deliver!(mail)
101-
smtp_from, smtp_to, message = check_delivery_params(mail)
99+
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)
102100

103101
smtp = Net::SMTP.new(settings[:address], settings[:port])
104102
if settings[:tls] || settings[:ssl]
@@ -128,7 +126,6 @@ def deliver!(mail)
128126
self
129127
end
130128
end
131-
132129

133130
private
134131

lib/mail/network/delivery_methods/smtp_connection.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,21 @@ module Mail
3838
#
3939
# mail.deliver!
4040
class SMTPConnection
41-
include Mail::CheckDeliveryParams
41+
attr_accessor :smtp, :settings
4242

4343
def initialize(values)
4444
raise ArgumentError.new('A Net::SMTP object is required for this delivery method') if values[:connection].nil?
4545
self.smtp = values[:connection]
4646
self.settings = values
4747
end
4848

49-
attr_accessor :smtp
50-
attr_accessor :settings
51-
5249
# Send the message via SMTP.
5350
# The from and to attributes are optional. If not set, they are retrieve from the Message.
5451
def deliver!(mail)
55-
smtp_from, smtp_to, message = check_delivery_params(mail)
52+
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)
5653
response = smtp.sendmail(message, smtp_from, smtp_to)
5754

5855
settings[:return_response] ? response : self
5956
end
60-
6157
end
6258
end

lib/mail/network/delivery_methods/test_mailer.rb

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ module Mail
88
# It also provides a template of the minimum methods you require to implement
99
# if you want to make a custom mailer for Mail
1010
class TestMailer
11-
include Mail::CheckDeliveryParams
12-
1311
# Provides a store of all the emails sent with the TestMailer so you can check them.
14-
def TestMailer.deliveries
12+
def self.deliveries
1513
@@deliveries ||= []
1614
end
1715

@@ -26,20 +24,19 @@ def TestMailer.deliveries
2624
# * length
2725
# * size
2826
# * and other common Array methods
29-
def TestMailer.deliveries=(val)
27+
def self.deliveries=(val)
3028
@@deliveries = val
3129
end
3230

31+
attr_accessor :settings
32+
3333
def initialize(values)
3434
@settings = values.dup
3535
end
36-
37-
attr_accessor :settings
3836

3937
def deliver!(mail)
40-
check_delivery_params(mail)
38+
Mail::CheckDeliveryParams.check(mail)
4139
Mail::TestMailer.deliveries << mail
4240
end
43-
4441
end
4542
end

spec/mail/network/delivery_methods/exim_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@
187187
subject "Email with no sender"
188188
body "body"
189189
end
190-
end.to raise_error('An SMTP From address is required to send a message. Set the message smtp_envelope_from, return_path, sender, or from address.')
190+
end.to raise_error('SMTP From address may not be blank: nil')
191191
end
192192

193193
it "should raise an error if no recipient if defined" do
@@ -200,6 +200,6 @@
200200
subject "Email with no recipient"
201201
body "body"
202202
end
203-
end.to raise_error('An SMTP To address is required to send a message. Set the message smtp_envelope_to, to, cc, or bcc address.')
203+
end.to raise_error('SMTP To address may not be blank: []')
204204
end
205205
end

spec/mail/network/delivery_methods/file_delivery_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
subject "Email with no sender"
102102
body "body"
103103
end
104-
end.to raise_error('An SMTP From address is required to send a message. Set the message smtp_envelope_from, return_path, sender, or from address.')
104+
end.to raise_error('SMTP From address may not be blank: nil')
105105
end
106106

107107
it "should raise an error if no recipient if defined" do
@@ -115,7 +115,7 @@
115115
subject "Email with no recipient"
116116
body "body"
117117
end
118-
end.to raise_error('An SMTP To address is required to send a message. Set the message smtp_envelope_to, to, cc, or bcc address.')
118+
end.to raise_error('SMTP To address may not be blank: []')
119119
end
120120

121121
end

spec/mail/network/delivery_methods/sendmail_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@
206206
subject "Email with no sender"
207207
body "body"
208208
end
209-
end.to raise_error('An SMTP From address is required to send a message. Set the message smtp_envelope_from, return_path, sender, or from address.')
209+
end.to raise_error('SMTP From address may not be blank: nil')
210210
end
211211

212212
it "should raise an error if no recipient if defined" do
@@ -219,6 +219,6 @@
219219
subject "Email with no recipient"
220220
body "body"
221221
end
222-
end.to raise_error('An SMTP To address is required to send a message. Set the message smtp_envelope_to, to, cc, or bcc address.')
222+
end.to raise_error('SMTP To address may not be blank: []')
223223
end
224224
end

0 commit comments

Comments
 (0)