Blob Blame History Raw
From ea7b67981156f3eaee8420bb34c49605573387a5 Mon Sep 17 00:00:00 2001
From: shugo <shugo@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
Date: Wed, 8 Jun 2016 07:06:57 +0000
Subject: [PATCH] Security: backport SMTP injection fix

* lib/net/smtp.rb (getok, get_response): raise an ArgumentError when
CR or LF is included in a line, because they are not allowed in
RFC5321.

https://hackerone.com/reports/137631
---
 ChangeLog                  |  6 ++++++
 lib/net/smtp.rb            |  9 +++++++++
 test/net/smtp/test_smtp.rb | 47 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 62 insertions(+)

diff --git a/ChangeLog b/ChangeLog
index ab9a6bf18281..5176d362881b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+Sun Jun 11 21:25:09 2017  Shugo Maeda  <shugo@ruby-lang.org>
+
+	* lib/net/smtp.rb (getok, get_response): raise an ArgumentError when
+	  CR or LF is included in a line, because they are not allowed in
+	  RFC5321. https://hackerone.com/reports/137631 [Backport 0827a7e]
+
 Wed Jul  5 15:55:35 2017  NAKAMURA Usaku  <usa@ruby-lang.org>
 
 	* ext/openssl/ossl_cipher.c: remove the encryption key initialization
diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb
index d634274c3ee8..78f2181d2a8b 100644
--- a/lib/net/smtp.rb
+++ b/lib/net/smtp.rb
@@ -926,7 +926,15 @@ def quit
 
     private
 
+    def validate_line(line)
+      # A bare CR or LF is not allowed in RFC5321.
+      if /[\r\n]/ =~ line
+        raise ArgumentError, "A line must not contain CR or LF"
+      end
+    end
+
     def getok(reqline)
+      validate_line reqline
       res = critical {
         @socket.writeline reqline
         recv_response()
@@ -936,6 +944,7 @@ def getok(reqline)
     end
 
     def get_response(reqline)
+      validate_line reqline
       @socket.writeline reqline
       recv_response()
     end
diff --git a/test/net/smtp/test_smtp.rb b/test/net/smtp/test_smtp.rb
index 0edb3419d56e..3bcceb6fc5bb 100644
--- a/test/net/smtp/test_smtp.rb
+++ b/test/net/smtp/test_smtp.rb
@@ -6,6 +6,8 @@
 module Net
   class TestSMTP < Test::Unit::TestCase
     class FakeSocket
+      attr_reader :write_io
+
       def initialize out = "250 OK\n"
         @write_io = StringIO.new
         @read_io  = StringIO.new out
@@ -51,5 +53,50 @@ def test_rset
 
       assert smtp.rset
     end
+
+    def test_mailfrom
+      sock = FakeSocket.new
+      smtp = Net::SMTP.new 'localhost', 25
+      smtp.instance_variable_set :@socket, sock
+      assert smtp.mailfrom("foo@example.com").success?
+      assert_equal "MAIL FROM:<foo@example.com>\r\n", sock.write_io.string
+    end
+
+    def test_rcptto
+      sock = FakeSocket.new
+      smtp = Net::SMTP.new 'localhost', 25
+      smtp.instance_variable_set :@socket, sock
+      assert smtp.rcptto("foo@example.com").success?
+      assert_equal "RCPT TO:<foo@example.com>\r\n", sock.write_io.string
+    end
+
+    def test_auth_plain
+      sock = FakeSocket.new
+      smtp = Net::SMTP.new 'localhost', 25
+      smtp.instance_variable_set :@socket, sock
+      assert smtp.auth_plain("foo", "bar").success?
+      assert_equal "AUTH PLAIN AGZvbwBiYXI=\r\n", sock.write_io.string
+    end
+
+    def test_crlf_injection
+      smtp = Net::SMTP.new 'localhost', 25
+      smtp.instance_variable_set :@socket, FakeSocket.new
+
+      assert_raise(ArgumentError) do
+        smtp.mailfrom("foo\r\nbar")
+      end
+
+      assert_raise(ArgumentError) do
+        smtp.mailfrom("foo\rbar")
+      end
+
+      assert_raise(ArgumentError) do
+        smtp.mailfrom("foo\nbar")
+      end
+
+      assert_raise(ArgumentError) do
+        smtp.rcptto("foo\r\nbar")
+      end
+    end
   end
 end