Blocking executable files (even buried inside ZIPs) in Exim

One of the handful of family members I host e-mail for had a narrow escape the other day, just about managing to avoid opening an .exe file buried inside a ZIP file attached to an e-mail purporting to be from Amazon.

The quality of some fake e-mails sloshing around these days is very, very good, and it seemed in this case that even the full might of SpamAssassin and ClamAV (with unofficial malware signatures) hadn’t sufficed to stop this one getting to the user’s inbox.

Spurred on by the thought of how long it might have taken me to disinfect their Windows box if they’d opened the .exe, I decided to take more drastic measures and block attachments containing .exes on the server.

Plenty of recipes for doing this are to be found on the net. The really nice bit for me, though, was the chance to break out Eximunit and do some test-driven sysadmin:

from eximunit import EximTestCase

from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Utils import COMMASPACE, formatdate
from email import Encoders

EXE_REJECT_MSG = """Executable attachments are not accepted. Contact postmaster if you have a
legitimate reason to send such files."""

ZIP_EXE_REJECT_MSG = """Executable attachments are not accepted, even inside ZIP files. Contact
postmaster if you have a legitimate reason to send such files."""

class ExeTests(EximTestCase):
    """Tests for .exe rejection"""

    def setUp(self):
        # Sets the default IP for sesions to be faked from
        self.setDefaultFromIP("10.0.0.1")

    def testDavidDomainRejectsExe(self):
        self.newSession().mailFrom('[email protected]')\
                         .rcptTo('[email protected]').assertDataRejected(self.messageWithAttachment('test.exe'), EXE_REJECT_MSG)

    def testDavidDomainRejectsExeZip(self):
        self.newSession().mailFrom('[email protected]')\
                         .rcptTo('[email protected]').assertDataRejected(self.messageWithAttachment('test.zip'), ZIP_EXE_REJECT_MSG)

    def testDavidDomainAcceptsJPG(self):
        self.newSession().mailFrom('[email protected]')\
                         .rcptTo('[email protected]').data(self.messageWithAttachment('test.jpg'))

    def testDavidDomainAcceptsJpgZip(self):
        self.newSession().mailFrom('[email protected]')\
                         .rcptTo('[email protected]').data(self.messageWithAttachment('contains-pics.zip'))

    def messageWithAttachment(self, filename):
        msg = MIMEMultipart()
        msg['From'] = '[email protected]'
        msg['To'] = COMMASPACE.join('[email protected]')
        msg['Date'] = formatdate(localtime=True)
        msg['Subject'] = 'This is a subject about .exe and or .zip'

        msg.attach(MIMEText('Test message body'))

        part = MIMEBase('application', "octet-stream")
        part.set_payload( open(filename,"rb").read() )
        Encoders.encode_base64(part)
        part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(filename))
        msg.attach(part)
        return msg.as_string()

The tests (both positive and negative cases) helped me to hammer out a couple of initial bugs. I really don’t know how anyone runs a live e-mail service without this sort of reassurance when tweaking the settings.

P.S. In the 48 hours since it went live, the new check has rejected over 60 messages, all of them containing a single .exe buried inside a ZIP. Many, but not all, of the messages are purporting to be from Amazon, and a surprising variety of different hosts are sending them, presumably part of a botnet of compromised machines.