Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.6.x SMTP security: prevent command injection via To/From addresses #1098

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rdoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
== Version 2.6.6 - unreleased

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

== Version 2.6.5 - 2017-04-26 Jeremy Daer <[email protected]>

Features:
Expand Down
57 changes: 47 additions & 10 deletions lib/mail/check_delivery_params.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ module Mail
#
# mail.deliver!
class SMTP
include Mail::CheckDeliveryParams
attr_accessor :settings

def initialize(values)
self.settings = { :address => "localhost",
Expand All @@ -91,12 +91,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 All @@ -120,7 +118,6 @@ def deliver!(mail)
self
end
end


private

Expand Down
8 changes: 2 additions & 6 deletions lib/mail/network/delivery_methods/smtp_connection.rb
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
61 changes: 58 additions & 3 deletions spec/mail/network/delivery_methods/smtp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,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 @@ -201,8 +201,63 @@ 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
end

it "should raise on SMTP injection via MAIL FROM newlines" do
addr = "[email protected]>\r\nDATA"

mail = Mail.new do
from addr
to "[email protected]"
end

# Mail 2.6.x header unfolding collapses whitespace, avoiding
# SMTP injection as a side effect.
expect(mail.smtp_envelope_from).to eq addr.gsub(/[\r\n]+/, ' ')
end

it "should raise on SMTP injection via RCPT TO newlines" do
addr = "[email protected]>\r\nDATA"

mail = Mail.new do
from "[email protected]"
to addr
end

# Mail 2.6.x header unfolding collapses whitespace, avoiding
# SMTP injection as a side effect.
expect(mail.smtp_envelope_to).to eq [addr.gsub(/[\r\n]+/, ' ')]
end

it "should raise on SMTP injection via MAIL FROM overflow" do
addr = "[email protected]#{'m' * 2025}DATA"

mail = Mail.new do
from addr
to "[email protected]"
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 = "[email protected]#{'m' * 2027}DATA"

mail = Mail.new do
from "[email protected]"
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
4 changes: 2 additions & 2 deletions spec/mail/network/delivery_methods/test_mailer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,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 @@ -78,7 +78,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

it "should save settings passed to initialize" do
Expand Down