CVE-2024-27938 (Ruby, published 2024-03-11T22:15Z)
Description: value: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/postalserver/postal
Files changed:
app/lib/smtp_server/client.rb
app/lib/smtp_server/server.rb
spec/lib/smtp_server/client/finished_spec.rb
Commit message: 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>`.


Vulnerable code:

        if idata == "."
            if idata.blank?
                # 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)

                while buffers[io].index("\r\n")
                  line, buffers[io] = buffers[io].split("\r\n", 2)

          expect(client.handle(".")).to eq "552 Message too large (maximum size 1MB)"
          expect(client.handle(".")).to eq "550 Loop detected"
          expect(client.handle(".")).to eq "530 From/Sender name is not valid"
          expect(client.handle(".")).to eq "250 OK"
            expect(client.handle(".")).to eq "250 OK"
            expect(client.handle(".")).to eq "250 OK"
          expect(client.handle(".")).to eq "250 OK"

Fixed code:

      @cr_present = false
      @previous_cr_present = nil

      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

    ensure
      @previous_cr_present = @cr_present
        if idata == "." && @cr_present && @previous_cr_present
            if idata&.length&.zero?
                while buffers[io].index("\n")
                  line, buffers[io] = buffers[io].split("\n", 2)
      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

          client.handle("\r")
          expect(client.handle(".\r")).to eq "552 Message too large (maximum size 1MB)"
          client.handle("\r")
          expect(client.handle(".\r")).to eq "550 Loop detected"
          client.handle("\r")
          expect(client.handle(".\r")).to eq "530 From/Sender name is not valid"
          client.handle("\r")
          expect(client.handle(".\r")).to eq "250 OK"
            client.handle("\r")
            expect(client.handle(".\r")).to eq "250 OK"
            client.handle("\r")
            expect(client.handle(".\r")).to eq "250 OK"
          client.handle("\r")
          expect(client.handle(".\r")).to eq "250 OK"