Skip to content

Commit

Permalink
SMTP security: prevent command injection via To/From addresses
Browse files Browse the repository at this point in the history
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

Closes #1097
  • Loading branch information
jeremy committed May 9, 2017
1 parent 5bcf5df commit cfff6c8
Show file tree
Hide file tree
Showing 13 changed files with 137 additions and 54 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rdoc
@@ -1,5 +1,8 @@
== HEAD

Security:
* #1097 – SMTP security: prevent command injection via To/From addresses. (jeremy)

Features:
* #804 - Configurable SMTP open_timeout and read_timeout. (ankane)
* #853 - `Mail::Message#set_sort_order` overrides the default message part sort order. (rafbm)
Expand Down
57 changes: 47 additions & 10 deletions lib/mail/check_delivery_params.rb
@@ -1,21 +1,58 @@
# frozen_string_literal: true
module Mail
module CheckDeliveryParams
def check_delivery_params(mail)
if Utilities.blank?(mail.smtp_envelope_from)
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.')
module CheckDeliveryParams #:nodoc:
class << self
def check(mail)
[ check_from(mail.smtp_envelope_from),
check_to(mail.smtp_envelope_to),
check_message(mail) ]
end

if Utilities.blank?(mail.smtp_envelope_to)
raise ArgumentError.new('An SMTP To address is required to send a message. Set the message smtp_envelope_to, to, cc, or bcc address.')
def check_from(addr)
if Utilities.blank?(addr)
raise ArgumentError, "SMTP From address may not be blank: #{addr.inspect}"
end

check_addr 'From', addr
end

def check_to(addrs)
if Utilities.blank?(addrs)
raise ArgumentError, "SMTP To address may not be blank: #{addrs.inspect}"
end

Array(addrs).map do |addr|
check_addr 'To', addr
end
end

message = mail.encoded if mail.respond_to?(:encoded)
if Utilities.blank?(message)
raise ArgumentError.new('An encoded message is required to send an email')
def check_addr(addr_name, addr)
validate_smtp_addr addr do |error_message|
raise ArgumentError, "SMTP #{addr_name} address #{error_message}: #{addr.inspect}"
end
end

[mail.smtp_envelope_from, mail.smtp_envelope_to, message]
def validate_smtp_addr(addr)
if addr.bytesize > 2048
yield 'may not exceed 2kB'
end

if /[\r\n]/ =~ addr
yield 'may not contain CR or LF line breaks'
end

addr
end

def check_message(message)
message = message.encoded if message.respond_to?(:encoded)

if Utilities.blank?(message)
raise ArgumentError, 'An encoded message is required to send an email'
end

message
end
end
end
end
12 changes: 4 additions & 8 deletions lib/mail/network/delivery_methods/file_delivery.rb
Expand Up @@ -2,7 +2,6 @@
require 'mail/check_delivery_params'

module Mail

# FileDelivery class delivers emails into multiple files based on the destination
# address. Each file is appended to if it already exists.
#
Expand All @@ -14,22 +13,20 @@ module Mail
# Make sure the path you specify with :location is writable by the Ruby process
# running Mail.
class FileDelivery
include Mail::CheckDeliveryParams

if RUBY_VERSION >= '1.9.1'
require 'fileutils'
else
require 'ftools'
end

attr_accessor :settings

def initialize(values)
self.settings = { :location => './mails' }.merge!(values)
end

attr_accessor :settings


def deliver!(mail)
check_delivery_params(mail)
Mail::CheckDeliveryParams.check(mail)

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

end
end
6 changes: 2 additions & 4 deletions lib/mail/network/delivery_methods/sendmail.rb
Expand Up @@ -38,17 +38,15 @@ module Mail
#
# mail.deliver!
class Sendmail
include Mail::CheckDeliveryParams
attr_accessor :settings

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

attr_accessor :settings

def deliver!(mail)
smtp_from, smtp_to, message = check_delivery_params(mail)
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)

from = "-f #{self.class.shellquote(smtp_from)}"
to = smtp_to.map { |_to| self.class.shellquote(_to) }.join(' ')
Expand Down
7 changes: 2 additions & 5 deletions lib/mail/network/delivery_methods/smtp.rb
Expand Up @@ -74,7 +74,7 @@ module Mail
#
# mail.deliver!
class SMTP
include Mail::CheckDeliveryParams
attr_accessor :settings

def initialize(values)
self.settings = { :address => "localhost",
Expand All @@ -93,12 +93,10 @@ def initialize(values)
}.merge!(values)
end

attr_accessor :settings

# Send the message via SMTP.
# The from and to attributes are optional. If not set, they are retrieve from the Message.
def deliver!(mail)
smtp_from, smtp_to, message = check_delivery_params(mail)
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)

smtp = Net::SMTP.new(settings[:address], settings[:port])
if settings[:tls] || settings[:ssl]
Expand Down Expand Up @@ -128,7 +126,6 @@ def deliver!(mail)
self
end
end


private

Expand Down
8 changes: 2 additions & 6 deletions lib/mail/network/delivery_methods/smtp_connection.rb
Expand Up @@ -38,25 +38,21 @@ module Mail
#
# mail.deliver!
class SMTPConnection
include Mail::CheckDeliveryParams
attr_accessor :smtp, :settings

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

attr_accessor :smtp
attr_accessor :settings

# Send the message via SMTP.
# The from and to attributes are optional. If not set, they are retrieve from the Message.
def deliver!(mail)
smtp_from, smtp_to, message = check_delivery_params(mail)
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)
response = smtp.sendmail(message, smtp_from, smtp_to)

settings[:return_response] ? response : self
end

end
end
13 changes: 5 additions & 8 deletions lib/mail/network/delivery_methods/test_mailer.rb
Expand Up @@ -8,10 +8,8 @@ module Mail
# It also provides a template of the minimum methods you require to implement
# if you want to make a custom mailer for Mail
class TestMailer
include Mail::CheckDeliveryParams

# Provides a store of all the emails sent with the TestMailer so you can check them.
def TestMailer.deliveries
def self.deliveries
@@deliveries ||= []
end

Expand All @@ -26,20 +24,19 @@ def TestMailer.deliveries
# * length
# * size
# * and other common Array methods
def TestMailer.deliveries=(val)
def self.deliveries=(val)
@@deliveries = val
end

attr_accessor :settings

def initialize(values)
@settings = values.dup
end

attr_accessor :settings

def deliver!(mail)
check_delivery_params(mail)
Mail::CheckDeliveryParams.check(mail)
Mail::TestMailer.deliveries << mail
end

end
end
4 changes: 2 additions & 2 deletions spec/mail/network/delivery_methods/exim_spec.rb
Expand Up @@ -187,7 +187,7 @@
subject "Email with no sender"
body "body"
end
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.')
end.to raise_error('SMTP From address may not be blank: nil')
end

it "should raise an error if no recipient if defined" do
Expand All @@ -200,6 +200,6 @@
subject "Email with no recipient"
body "body"
end
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.')
end.to raise_error('SMTP To address may not be blank: []')
end
end
4 changes: 2 additions & 2 deletions spec/mail/network/delivery_methods/file_delivery_spec.rb
Expand Up @@ -101,7 +101,7 @@
subject "Email with no sender"
body "body"
end
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.')
end.to raise_error('SMTP From address may not be blank: nil')
end

it "should raise an error if no recipient if defined" do
Expand All @@ -115,7 +115,7 @@
subject "Email with no recipient"
body "body"
end
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.')
end.to raise_error('SMTP To address may not be blank: []')
end

end
Expand Down
4 changes: 2 additions & 2 deletions spec/mail/network/delivery_methods/sendmail_spec.rb
Expand Up @@ -206,7 +206,7 @@
subject "Email with no sender"
body "body"
end
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.')
end.to raise_error('SMTP From address may not be blank: nil')
end

it "should raise an error if no recipient if defined" do
Expand All @@ -219,6 +219,6 @@
subject "Email with no recipient"
body "body"
end
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.')
end.to raise_error('SMTP To address may not be blank: []')
end
end
4 changes: 2 additions & 2 deletions spec/mail/network/delivery_methods/smtp_connection_spec.rb
Expand Up @@ -60,7 +60,7 @@
subject "Email with no sender"
body "body"
end
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.')
end.to raise_error('SMTP From address may not be blank: nil')
end

it "should raise an error if no recipient if defined" do
Expand All @@ -75,6 +75,6 @@
subject "Email with no recipient"
body "body"
end
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.')
end.to raise_error('SMTP To address may not be blank: []')
end
end
65 changes: 62 additions & 3 deletions spec/mail/network/delivery_methods/smtp_spec.rb
Expand Up @@ -270,7 +270,7 @@ def redefine_verify_none(new_value)
subject "Email with no sender"
body "body"
end
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.')
end.to raise_error('SMTP From address may not be blank: nil')
end

it "should raise an error if no recipient if defined" do
Expand All @@ -280,8 +280,67 @@ def redefine_verify_none(new_value)
subject "Email with no recipient"
body "body"
end
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.')
end.to raise_error('SMTP To address may not be blank: []')
end

it "should raise on SMTP injection via MAIL FROM newlines" do
addr = "inject.from@example.com>\r\nDATA"

mail = Mail.new do
from addr
to "to@somemail.com"
end

expect(mail.smtp_envelope_from).to eq addr

expect do
mail.deliver
end.to raise_error(ArgumentError, "SMTP From address may not contain CR or LF line breaks: #{addr.inspect}")
end

it "should raise on SMTP injection via RCPT TO newlines" do
addr = "inject.to@example.com>\r\nDATA"

mail = Mail.new do
from "from@somemail.com"
to addr
end

expect(mail.smtp_envelope_to).to eq [addr]

expect do
mail.deliver
end.to raise_error(ArgumentError, "SMTP To address may not contain CR or LF line breaks: #{addr.inspect}")
end
end

it "should raise on SMTP injection via MAIL FROM overflow" do
addr = "inject.from@example.com#{'m' * 2025}DATA"

mail = Mail.new do
from addr
to "to@somemail.com"
end

expect(mail.smtp_envelope_from).to eq addr

expect do
mail.deliver
end.to raise_error(ArgumentError, "SMTP From address may not exceed 2kB: #{addr.inspect}")
end

it "should raise on SMTP injection via RCPT TO overflow" do
addr = "inject.to@example.com#{'m' * 2027}DATA"

mail = Mail.new do
from "from@somemail.com"
to addr
end

expect(mail.smtp_envelope_to).to eq [addr]

expect do
mail.deliver
end.to raise_error(ArgumentError, "SMTP To address may not exceed 2kB: #{addr.inspect}")
end
end
end

0 comments on commit cfff6c8

Please sign in to comment.