CVE-2024-27938 (published 2024-03-11T22:15Z)

Postal is an open source SMTP server. Postal versions less than 3.0.0 are vulnerable to SMTP Smuggling attacks which may allow incoming e-mails to be spoofed. This, in conjunction with a cooperative outgoing SMTP service, would allow for an incoming e-mail to be received by Postal addressed from a server that a user has 'authorised' to send mail on their behalf but were not the genuine author of the e-mail. Postal is not affected for sending outgoing e-mails as email is re-encoded with `<CR><LF>` line endings when transmitted over SMTP. This issue has been addressed and users should upgrade to Postal v3.0.0 or higher. Once upgraded, Postal will only accept End of DATA sequences which are explicitly `<CR><LF>.<CR><LF>`. If a non-compliant sequence is detected it will be logged to the SMTP server log. There are no workarounds for this issue.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/postalserver/postal
commit_details: repo_url: https://github.com/postalserver/postal
author: Adam Cooke
author_date: 2024-03-01 17:12:10+00:00
msg: feat: only accept RFC-compliant End-of-DATA sequence

Previously Postal was fairly forgiving about line endings. This requires that the end of data sequence is `<CR><LF>.<CR><LF>`.
repository: repo_url: https://github.com/postalserver/postal
repo_name: postalserver/postal
description: 📮 A fully featured open source mail delivery platform for incoming & outgoing e-mail
repo_language: Ruby
owner: postalserver
file_changes: filename: client.rb
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,10 @@ module SMTPServer
     def initialize(ip_address)
       @logging_enabled = true
       @ip_address = ip_address
+
+      @cr_present = false
+      @previous_cr_present = nil
+
       if @ip_address
         check_ip_address
         @state = :welcome
@@ -51,6 +55,14 @@ module SMTPServer
     end
 
     def handle(data)
+      if data[-1] == "\r"
+        @cr_present = true
+        data = data.chop # remove last character (\r)
+      else
+        Postal.logger.debug("\e[33m   WARN: Detected line with invalid line ending (missing <CR>)\e[0m", id: id)
+        @cr_present = false
+      end
+
       Postal.logger.tagged(id: id) do
         if @state == :preauth
           return proxy(data)
@@ -59,11 +71,12 @@ module SMTPServer
         log "\e[32m<= #{sanitize_input_for_log(data.strip)}\e[0m"
         if @proc
           @proc.call(data)
-
         else
           handle_command(data)
         end
       end
+    ensure
+      @previous_cr_present = @cr_present
     end
 
     def finished?
@@ -409,7 +422,7 @@ module SMTPServer
       @headers["received"] = [received_header]
 
       handler = proc do |idata|
-        if idata == "."
+        if idata == "." && @cr_present && @previous_cr_present
           @logging_enabled = true
           @proc = nil
           finished
@@ -424,7 +437,7 @@ module SMTPServer
           end
 
           if @receiving_headers
-            if idata.blank?
+            if idata&.length&.zero?
               @receiving_headers = false
             elsif idata.to_s =~ /^\s/
               # This is a continuation of a header

programming_language: Ruby
filename: server.rb
change_type: ModificationType.MODIFY
diff: @@ -194,16 +194,11 @@ module SMTPServer
                   eof = true
                 end
 
-                # Normalize all \r\n and \n to \r\n, but ignore only \r.
-                # A \r\n may be split in 2 buffers (\n in one buffer and \r in the other)
-                buffers[io] = buffers[io].gsub(/\r/, "").encode(buffers[io].encoding, crlf_newline: true)
-
                 # We line buffer, so look to see if we have received a newline
                 # and keep doing so until all buffered lines have been processed.
-                while buffers[io].index("\r\n")
+                while buffers[io].index("\n")
                   # Extract the line
-                  line, buffers[io] = buffers[io].split("\r\n", 2)
-
+                  line, buffers[io] = buffers[io].split("\n", 2)
                   # Send the received line to the client object for processing
                   result = client.handle(line)
                   # If the client object returned some data, write it back to the client

programming_language: Ruby
filename: finished_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -22,12 +22,32 @@ module SMTPServer
     end
 
     describe "when finished sending data" do
+      context "when the . character does not end with a <CR>" do
+        it "does nothing" do
+          allow(Postal::Config.smtp_server).to receive(:max_message_size).and_return(1)
+          client.handle("DATA")
+          client.handle("Subject: Hello")
+          client.handle("\r")
+          expect(client.handle(".")).to be nil
+        end
+      end
+
+      context "when the data before the . character does not end with a <CR>" do
+        it "does nothing" do
+          allow(Postal::Config.smtp_server).to receive(:max_message_size).and_return(1)
+          client.handle("DATA")
+          client.handle("Subject: Hello")
+          expect(client.handle(".\r")).to be nil
+        end
+      end
+
       context "when the data is larger than the maximum message size" do
         it "returns an error and resets the state" do
           allow(Postal::Config.smtp_server).to receive(:max_message_size).and_return(1)
           client.handle("DATA")
           client.handle("a" * 1024 * 1024 * 10)
-          expect(client.handle(".")).to eq "552 Message too large (maximum size 1MB)"
+          client.handle("\r")
+          expect(client.handle(".\r")).to eq "552 Message too large (maximum size 1MB)"
         end
       end
 
@@ -43,7 +63,8 @@ module SMTPServer
           client.handle("To: #{rcpt_to}")
           client.handle("")
           client.handle("This is a test message")
-          expect(client.handle(".")).to eq "550 Loop detected"
+          client.handle("\r")
+          expect(client.handle(".\r")).to eq "550 Loop detected"
         end
       end
 
@@ -55,7 +76,8 @@ module SMTPServer
           client.handle("To: #{rcpt_to}")
           client.handle("")
           client.handle("This is a test message")
-          expect(client.handle(".")).to eq "530 From/Sender name is not valid"
+          client.handle("\r")
+          expect(client.handle(".\r")).to eq "530 From/Sender name is not valid"
         end
       end
 
@@ -71,7 +93,8 @@ module SMTPServer
           client.handle("To: #{rcpt_to}")
           client.handle("")
           client.handle("This is a test message")
-          expect(client.handle(".")).to eq "250 OK"
+          client.handle("\r")
+          expect(client.handle(".\r")).to eq "250 OK"
           queued_message = QueuedMessage.first
           expect(queued_message).to have_attributes(
             domain: "example.com",
@@ -110,7 +133,8 @@ module SMTPServer
             client.handle("To: #{rcpt_to}")
             client.handle("")
             client.handle("This is a test message")
-            expect(client.handle(".")).to eq "250 OK"
+            client.handle("\r")
+            expect(client.handle(".\r")).to eq "250 OK"
 
             queued_message = QueuedMessage.first
             expect(queued_message).to have_attributes(
@@ -141,7 +165,8 @@ module SMTPServer
             client.handle("To: #{rcpt_to}")
             client.handle("")
             client.handle("This is a test message")
-            expect(client.handle(".")).to eq "250 OK"
+            client.handle("\r")
+            expect(client.handle(".\r")).to eq "250 OK"
 
             queued_message = QueuedMessage.first
             expect(queued_message).to have_attributes(
@@ -179,7 +204,8 @@ module SMTPServer
           client.handle("To: #{rcpt_to}")
           client.handle("")
           client.handle("This is a test message")
-          expect(client.handle(".")).to eq "250 OK"
+          client.handle("\r")
+          expect(client.handle(".\r")).to eq "250 OK"
 
           queued_message = QueuedMessage.first
           expect(queued_message).to have_attributes(

programming_language: Ruby