CVE-2015-9096: Improper Neutralization of CRLF Sequences ('CRLF Injection') (CWE-93)
value: Net::SMTP in Ruby before 2.4.0 is vulnerable to SMTP command injection via CRLF sequences in a RCPT TO or MAIL FROM command, as demonstrated by CRLF sequences immediately before and after a DATA substring.
CWE description: The product uses CRLF (carriage return line feeds) as a special element, e.g. to separate lines or records, but it does not neutralize or incorrectly neutralizes CRLF sequences from inputs.
CVSS v3 base score: 6.1
Published: 2017-06-12T20:29Z
Repository: https://github.com/ruby/ruby
Language: Ruby
Files changed: ChangeLog, lib/net/smtp.rb, test/net/smtp/test_smtp.rb
Commit date: 2016-06-08 07:06:57 +0000
Commit message: * 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.

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@55324 b2dd03c8-39d4-4d8f-98ff-823fe69b080e

Diff: diff --git a/ChangeLog b/ChangeLog
index 2a1cb8c115..768469ea8e 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,11 @@
+Wed Jun  8 16:03:09 2016  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.
+
 Tue Jun  7 21:27:25 2016  Kazuki Yamaguchi  <k@rhe.jp>
 
 	* test/rubygems/*_{cert,cert_32}.pem: Regenerate test certificates for
 	  OpenSSL 1.1.0. This is already in upstream.
 	  https://github.com/rubygems/rubygems/commit/9be5c53939440a61c4bba73cfffbeb5cfadf72be
diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb
index 250293bdbe..a7130a593b 100644
--- a/lib/net/smtp.rb
+++ b/lib/net/smtp.rb
@@ -924,20 +924,29 @@ def quit
       getok('QUIT')
     end
 
     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()
       }
       check_response res
       res
     end
 
     def get_response(reqline)
+      validate_line reqline
       @socket.writeline reqline
       recv_response()
     end
 
     def recv_response
diff --git a/test/net/smtp/test_smtp.rb b/test/net/smtp/test_smtp.rb
index 0edb3419d5..3bcceb6fc5 100644
--- a/test/net/smtp/test_smtp.rb
+++ b/test/net/smtp/test_smtp.rb
@@ -4,10 +4,12 @@
 require 'test/unit'
 
 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
       end
 
@@ -49,7 +51,52 @@ def test_rset
       smtp = Net::SMTP.new 'localhost', 25
       smtp.instance_variable_set :@socket, FakeSocket.new
 
       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

Fixed code: Wed Jun  8 16:03:09 2016  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.

    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

      validate_line reqline
      validate_line reqline
      attr_reader :write_io


    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