CVE-2015-9097: Improper Neutralization of CRLF Sequences ('CRLF Injection') (CWE-93)
value: The mail gem before 2.5.5 for Ruby (aka A Really Ruby Mail Library) 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/mikel/mail
Language: Ruby
Files changed: lib/mail/field.rb, lib/mail/header.rb, lib/mail/patterns.rb, spec/mail/message_spec.rb
Commit date: 2013-02-01 23:32:24 -0800
Commit message: Also unfold and split lazily

This has little effect on the specs, but on my header reading example it
makes about a 10x performance difference, finally bringing it within one
order of magnitude of the "fast hacky solution" at
https://gist.github.com/5901bbd810c08ed3d0b1

Diff: diff --git a/lib/mail/field.rb b/lib/mail/field.rb
index 0fced8f2..c5a55a36 100644
--- a/lib/mail/field.rb
+++ b/lib/mail/field.rb
@@ -113,33 +113,35 @@ module Mail
     #  Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
     def initialize(name, value = nil, charset = 'utf-8')
       case
       when name =~ /:/                  # Field.new("field-name: field data")
         @charset = value.blank? ? charset : value
-        @name, @value = split(name)
+        @name = name[FIELD_PREFIX]
+        @raw_value = name
       when name !~ /:/ && value.blank?  # Field.new("field-name")
         @name = name
         @value = nil
         @charset = charset
       else                              # Field.new("field-name", "value")
         @name = name
         @value = value
         @charset = charset
       end
-      return self
+      @name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
     end
 
     def field=(value)
       @field = value
     end
 
     def field
+      _, @value = split(@raw_value) if @raw_value && !@value
       @field ||= create_field(@name, @value, @charset)
     end
 
     def name
-      FIELD_NAME_MAP[@name.to_s.downcase] || @name
+      @name
     end
 
     def value
       field.value
     end
@@ -196,11 +198,25 @@ module Mail
       [match_data[1].to_s.mb_chars.strip, match_data[2].to_s.mb_chars.strip]
     rescue
       STDERR.puts "WARNING: Could not parse (and so ignoring) '#{raw_field}'"
     end
 
+    # 2.2.3. Long Header Fields
+    #
+    #  The process of moving from this folded multiple-line representation
+    #  of a header field to its single line representation is called
+    #  "unfolding". Unfolding is accomplished by simply removing any CRLF
+    #  that is immediately followed by WSP.  Each header field should be
+    #  treated in its unfolded form for further syntactic and semantic
+    #  evaluation.
+    def unfold(string)
+      string.gsub(/[\r\n \t]+/m, ' ')
+    end
+
     def create_field(name, value, charset)
+      value = unfold(value) if value.is_a?(String) || value.is_a?(Mail::Multibyte::Chars)
+
       begin
         new_field(name, value, charset)
       rescue Mail::Field::ParseError => e
         field = Mail::UnstructuredField.new(name, value)
         field.errors << [name, value, e]
diff --git a/lib/mail/header.rb b/lib/mail/header.rb
index 6d5d80fb..3f6f5b15 100644
--- a/lib/mail/header.rb
+++ b/lib/mail/header.rb
@@ -242,31 +242,14 @@ module Mail
     
     def raw_source=(val)
       @raw_source = val
     end
     
-    # 2.2.3. Long Header Fields
-    # 
-    #  The process of moving from this folded multiple-line representation
-    #  of a header field to its single line representation is called
-    #  "unfolding". Unfolding is accomplished by simply removing any CRLF
-    #  that is immediately followed by WSP.  Each header field should be
-    #  treated in its unfolded form for further syntactic and semantic
-    #  evaluation.
-    def unfold(string)
-      string.gsub(/#{CRLF}#{WSP}+/, ' ').gsub(/#{WSP}+/, ' ')
-    end
-    
-    # Returns the header with all the folds removed
-    def unfolded_header
-      @unfolded_header ||= unfold(raw_source)
-    end
-    
     # Splits an unfolded and line break cleaned header into individual field
     # strings.
     def split_header
-      self.fields = unfolded_header.split(CRLF)
+      self.fields = raw_source.split(HEADER_SPLIT)
     end
     
     def select_field_for(name)
       fields.select { |f| f.responsible_for?(name) }
     end
diff --git a/lib/mail/patterns.rb b/lib/mail/patterns.rb
index 582fe1ae..77fcfc7b 100644
--- a/lib/mail/patterns.rb
+++ b/lib/mail/patterns.rb
@@ -18,14 +18,16 @@ module Mail
     CRLF          = /\r\n/
     WSP           = /[#{white_space}]/
     FWS           = /#{CRLF}#{WSP}*/
     TEXT          = /[#{text}]/ # + obs-text
     FIELD_NAME    = /[#{field_name}]+/
-    FIELD_BODY    = /.+/
+    FIELD_PREFIX  = /\A(#{FIELD_NAME})/
+    FIELD_BODY    = /.+/m
     FIELD_LINE    = /^[#{field_name}]+:\s*.+$/
     FIELD_SPLIT   = /^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})?$/
     HEADER_LINE   = /^([#{field_name}]+:\s*.+)$/
+    HEADER_SPLIT  = /#{CRLF}(?!#{WSP})/
 
     QP_UNSAFE     = /[^#{qp_safe}]/
     QP_SAFE       = /[#{qp_safe}]/
     CONTROL_CHAR  = /[#{control}]/n
     ATOM_UNSAFE   = /[#{Regexp.quote aspecial}#{control}#{sp}]/n
diff --git a/spec/mail/message_spec.rb b/spec/mail/message_spec.rb
index 68a3d96f..26849dca 100644
--- a/spec/mail/message_spec.rb
+++ b/spec/mail/message_spec.rb
@@ -107,11 +107,11 @@ describe Mail::Message do
       Mail.read(fixture('emails', 'plain_emails', 'raw_email_string_in_date_field.eml'))
     end
 
     it "should raise a warning (and keep parsing) on having an incorrectly formatted header" do
       STDERR.should_receive(:puts).with("WARNING: Could not parse (and so ignoring) 'quite Delivered-To: xxx@xxx.xxx'")
-      Mail.read(fixture('emails', 'plain_emails', 'raw_email_incorrect_header.eml'))
+      Mail.read(fixture('emails', 'plain_emails', 'raw_email_incorrect_header.eml')).to_s
     end
 
     it "should read in an email message and basically parse it" do
       mail = Mail.read(fixture('emails', 'plain_emails', 'basic_email.eml'))
       mail.to.should eq ["raasdnil@gmail.com"]

Vulnerable code:         @name, @value = split(name)
      return self
      FIELD_NAME_MAP[@name.to_s.downcase] || @name
    # 2.2.3. Long Header Fields
    # 
    #  The process of moving from this folded multiple-line representation
    #  of a header field to its single line representation is called
    #  "unfolding". Unfolding is accomplished by simply removing any CRLF
    #  that is immediately followed by WSP.  Each header field should be
    #  treated in its unfolded form for further syntactic and semantic
    #  evaluation.
    def unfold(string)
      string.gsub(/#{CRLF}#{WSP}+/, ' ').gsub(/#{WSP}+/, ' ')
    end
    
    # Returns the header with all the folds removed
    def unfolded_header
      @unfolded_header ||= unfold(raw_source)
    end
    
      self.fields = unfolded_header.split(CRLF)
    FIELD_BODY    = /.+/
      Mail.read(fixture('emails', 'plain_emails', 'raw_email_incorrect_header.eml'))
Fixed code:         @name = name[FIELD_PREFIX]
        @raw_value = name
      @name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
      _, @value = split(@raw_value) if @raw_value && !@value
      @name
    # 2.2.3. Long Header Fields
    #
    #  The process of moving from this folded multiple-line representation
    #  of a header field to its single line representation is called
    #  "unfolding". Unfolding is accomplished by simply removing any CRLF
    #  that is immediately followed by WSP.  Each header field should be
    #  treated in its unfolded form for further syntactic and semantic
    #  evaluation.
    def unfold(string)
      string.gsub(/[\r\n \t]+/m, ' ')
    end

      value = unfold(value) if value.is_a?(String) || value.is_a?(Mail::Multibyte::Chars)

      self.fields = raw_source.split(HEADER_SPLIT)
    FIELD_PREFIX  = /\A(#{FIELD_NAME})/
    FIELD_BODY    = /.+/m
    HEADER_SPLIT  = /#{CRLF}(?!#{WSP})/
      Mail.read(fixture('emails', 'plain_emails', 'raw_email_incorrect_header.eml')).to_s